namespace/
lib.rs

1// Copyright 2023 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.
4
5//! `namespace` defines namespace types and transformations between their common representations.
6
7use cm_types::{IterablePath, NamespacePath};
8use fidl::endpoints::ClientEnd;
9use thiserror::Error;
10use {
11    fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_runner as fcrunner,
12    fidl_fuchsia_io as fio, fidl_fuchsia_process as fprocess,
13};
14
15#[cfg(target_os = "fuchsia")]
16use std::sync::Arc;
17
18mod tree;
19pub use tree::Tree;
20
21/// The namespace of a component instance.
22///
23/// Namespaces may be represented as a collection of directory client endpoints and their
24/// corresponding unique paths, so called "flat representation". In this case each path
25/// must be a valid [`cm_types::NamespacePath`], and no path can be a parent of another path.
26///
27/// See https://fuchsia.dev/fuchsia-src/concepts/process/namespaces for the definition
28/// of namespaces of a process. The namespace of a component largely follows except that
29/// some more characters are disallowed (c.f. [`cm_types::NamespacePath`] documentation).
30#[derive(Debug)]
31pub struct Namespace {
32    tree: Tree<ClientEnd<fio::DirectoryMarker>>,
33}
34
35#[derive(Error, Debug, Clone)]
36pub enum NamespaceError {
37    #[error(
38        "path `{0}` is the parent or child of another namespace entry. \
39        This is not supported."
40    )]
41    Shadow(NamespacePath),
42
43    #[error("duplicate namespace path `{0}`")]
44    Duplicate(NamespacePath),
45
46    #[error("invalid namespace entry `{0}`")]
47    EntryError(#[from] EntryError),
48}
49
50impl Namespace {
51    pub fn new() -> Self {
52        Self { tree: Default::default() }
53    }
54
55    pub fn add(
56        &mut self,
57        path: &NamespacePath,
58        directory: ClientEnd<fio::DirectoryMarker>,
59    ) -> Result<(), NamespaceError> {
60        self.tree.add(path, directory)?;
61        Ok(())
62    }
63
64    pub fn get(&self, path: &NamespacePath) -> Option<&ClientEnd<fio::DirectoryMarker>> {
65        self.tree.get(path)
66    }
67
68    pub fn remove(&mut self, path: &NamespacePath) -> Option<ClientEnd<fio::DirectoryMarker>> {
69        self.tree.remove(path)
70    }
71
72    pub fn flatten(self) -> Vec<Entry> {
73        self.tree.flatten().into_iter().map(|(path, directory)| Entry { path, directory }).collect()
74    }
75
76    /// Get a copy of the paths in the namespace.
77    pub fn paths(&self) -> Vec<NamespacePath> {
78        self.tree.map_ref(|_| ()).flatten().into_iter().map(|(path, ())| path).collect()
79    }
80}
81
82impl Default for Namespace {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88#[cfg(target_os = "fuchsia")]
89impl Clone for Namespace {
90    fn clone(&self) -> Self {
91        // TODO(https://fxbug.dev/42083023): The unsafe block can go away if Rust FIDL bindings exposed the
92        // feature of calling FIDL methods (e.g. Clone) on a borrowed client endpoint.
93        let tree = self.tree.map_ref(|dir| {
94            let raw_handle = dir.channel().as_handle_ref().raw_handle();
95            // SAFETY: the channel is forgotten at the end of scope so it is not double closed.
96            unsafe {
97                let borrowed: zx::Channel = zx::NullableHandle::from_raw(raw_handle).into();
98                let borrowed = fio::DirectorySynchronousProxy::new(borrowed);
99                let (client_end, server_end) =
100                    fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
101                let _ = borrowed.clone(server_end.into_channel().into());
102                std::mem::forget(borrowed.into_channel());
103                client_end
104            }
105        });
106        Self { tree }
107    }
108}
109
110impl From<Namespace> for Vec<Entry> {
111    fn from(namespace: Namespace) -> Self {
112        namespace.flatten()
113    }
114}
115
116impl From<Namespace> for Vec<fcrunner::ComponentNamespaceEntry> {
117    fn from(namespace: Namespace) -> Self {
118        namespace.flatten().into_iter().map(Into::into).collect()
119    }
120}
121
122impl From<Namespace> for Vec<fprocess::NameInfo> {
123    fn from(namespace: Namespace) -> Self {
124        namespace.flatten().into_iter().map(Into::into).collect()
125    }
126}
127
128#[cfg(target_os = "fuchsia")]
129impl From<Namespace> for Vec<process_builder::NamespaceEntry> {
130    fn from(namespace: Namespace) -> Self {
131        namespace.flatten().into_iter().map(Into::into).collect()
132    }
133}
134
135/// Converts the [Namespace] to a vfs [TreeBuilder] with tree nodes for each entry.
136///
137/// The TreeBuilder can then be used to build a vfs directory for this Namespace.
138#[cfg(target_os = "fuchsia")]
139impl TryFrom<Namespace> for vfs::tree_builder::TreeBuilder {
140    type Error = vfs::tree_builder::Error;
141
142    fn try_from(namespace: Namespace) -> Result<Self, Self::Error> {
143        let mut builder = vfs::tree_builder::TreeBuilder::empty_dir();
144        for Entry { path, directory } in namespace.flatten().into_iter() {
145            let path: Vec<&str> = path.iter_segments().map(|s| s.as_str()).collect();
146            builder.add_entry(path, vfs::remote::remote_dir(directory.into_proxy()))?;
147        }
148        Ok(builder)
149    }
150}
151
152/// Converts the [Namespace] into a vfs directory.
153#[cfg(target_os = "fuchsia")]
154impl TryFrom<Namespace> for Arc<vfs::directory::immutable::simple::Simple> {
155    type Error = vfs::tree_builder::Error;
156
157    fn try_from(namespace: Namespace) -> Result<Self, Self::Error> {
158        let builder: vfs::tree_builder::TreeBuilder = namespace.try_into()?;
159        Ok(builder.build())
160    }
161}
162
163impl From<Tree<ClientEnd<fio::DirectoryMarker>>> for Namespace {
164    fn from(tree: Tree<ClientEnd<fio::DirectoryMarker>>) -> Self {
165        Self { tree }
166    }
167}
168
169impl TryFrom<Vec<Entry>> for Namespace {
170    type Error = NamespaceError;
171
172    fn try_from(value: Vec<Entry>) -> Result<Self, Self::Error> {
173        let mut ns = Namespace::new();
174        for entry in value {
175            ns.add(&entry.path, entry.directory)?;
176        }
177        Ok(ns)
178    }
179}
180
181impl TryFrom<Vec<fcrunner::ComponentNamespaceEntry>> for Namespace {
182    type Error = NamespaceError;
183
184    fn try_from(entries: Vec<fcrunner::ComponentNamespaceEntry>) -> Result<Self, Self::Error> {
185        let entries: Vec<Entry> =
186            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
187        entries.try_into()
188    }
189}
190
191impl TryFrom<Vec<fcomponent::NamespaceEntry>> for Namespace {
192    type Error = NamespaceError;
193
194    fn try_from(entries: Vec<fcomponent::NamespaceEntry>) -> Result<Self, Self::Error> {
195        let entries: Vec<Entry> =
196            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
197        entries.try_into()
198    }
199}
200
201impl TryFrom<Vec<fprocess::NameInfo>> for Namespace {
202    type Error = NamespaceError;
203
204    fn try_from(entries: Vec<fprocess::NameInfo>) -> Result<Self, Self::Error> {
205        let entries: Vec<Entry> =
206            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
207        entries.try_into()
208    }
209}
210
211#[cfg(target_os = "fuchsia")]
212impl TryFrom<Vec<process_builder::NamespaceEntry>> for Namespace {
213    type Error = NamespaceError;
214
215    fn try_from(entries: Vec<process_builder::NamespaceEntry>) -> Result<Self, Self::Error> {
216        let entries: Vec<Entry> =
217            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
218        entries.try_into()
219    }
220}
221
222/// A container for a single namespace entry, containing a path and a directory handle.
223#[derive(Eq, Ord, PartialOrd, PartialEq, Debug)]
224pub struct Entry {
225    /// Namespace path.
226    pub path: NamespacePath,
227
228    /// Namespace directory handle.
229    pub directory: ClientEnd<fio::DirectoryMarker>,
230}
231
232impl From<Entry> for fcrunner::ComponentNamespaceEntry {
233    fn from(entry: Entry) -> Self {
234        Self {
235            path: Some(entry.path.into()),
236            directory: Some(entry.directory),
237            ..Default::default()
238        }
239    }
240}
241
242impl From<Entry> for fcomponent::NamespaceEntry {
243    fn from(entry: Entry) -> Self {
244        Self {
245            path: Some(entry.path.into()),
246            directory: Some(entry.directory),
247            ..Default::default()
248        }
249    }
250}
251
252impl From<Entry> for fprocess::NameInfo {
253    fn from(entry: Entry) -> Self {
254        Self { path: entry.path.into(), directory: entry.directory }
255    }
256}
257
258#[cfg(target_os = "fuchsia")]
259impl From<Entry> for process_builder::NamespaceEntry {
260    fn from(entry: Entry) -> Self {
261        Self { path: entry.path.into(), directory: entry.directory }
262    }
263}
264
265#[derive(Debug, Clone, Error)]
266pub enum EntryError {
267    #[error("path is not set")]
268    MissingPath,
269
270    #[error("directory is not set")]
271    MissingDirectory,
272
273    #[error("entry type is not supported (must be directory or dictionary")]
274    UnsupportedType,
275
276    #[error("path is invalid for a namespace entry: `{0}`")]
277    InvalidPath(#[from] cm_types::ParseError),
278}
279
280impl TryFrom<fcrunner::ComponentNamespaceEntry> for Entry {
281    type Error = EntryError;
282
283    fn try_from(entry: fcrunner::ComponentNamespaceEntry) -> Result<Self, Self::Error> {
284        Ok(Self {
285            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
286            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
287        })
288    }
289}
290
291impl TryFrom<fcomponent::NamespaceEntry> for Entry {
292    type Error = EntryError;
293
294    fn try_from(entry: fcomponent::NamespaceEntry) -> Result<Self, Self::Error> {
295        Ok(Self {
296            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
297            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
298        })
299    }
300}
301
302impl TryFrom<fprocess::NameInfo> for Entry {
303    type Error = EntryError;
304
305    fn try_from(entry: fprocess::NameInfo) -> Result<Self, Self::Error> {
306        Ok(Self { path: entry.path.parse()?, directory: entry.directory })
307    }
308}
309
310#[cfg(target_os = "fuchsia")]
311impl TryFrom<process_builder::NamespaceEntry> for Entry {
312    type Error = EntryError;
313
314    fn try_from(entry: process_builder::NamespaceEntry) -> Result<Self, Self::Error> {
315        Ok(Self { path: entry.path.try_into()?, directory: entry.directory })
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use assert_matches::assert_matches;
323    use fidl::endpoints::Proxy as _;
324    use fuchsia_async as fasync;
325    use zx::{AsHandleRef, Peered};
326
327    fn ns_path(str: &str) -> NamespacePath {
328        str.parse().unwrap()
329    }
330
331    #[test]
332    fn test_try_from_namespace() {
333        {
334            let (client_end_1, _) = fidl::endpoints::create_endpoints();
335            let (client_end_2, _) = fidl::endpoints::create_endpoints();
336            let entries = vec![
337                Entry { path: ns_path("/foo"), directory: client_end_1 },
338                Entry { path: ns_path("/foo/bar"), directory: client_end_2 },
339            ];
340            assert_matches!(
341                Namespace::try_from(entries),
342                Err(NamespaceError::Shadow(path)) if path.to_string() == "/foo/bar"
343            );
344        }
345        {
346            let (client_end_1, _) = fidl::endpoints::create_endpoints();
347            let (client_end_2, _) = fidl::endpoints::create_endpoints();
348            let entries = vec![
349                Entry { path: ns_path("/foo"), directory: client_end_1 },
350                Entry { path: ns_path("/foo"), directory: client_end_2 },
351            ];
352            assert_matches!(
353                Namespace::try_from(entries),
354                Err(NamespaceError::Duplicate(path)) if path.to_string() == "/foo"
355            );
356        }
357    }
358
359    #[cfg(target_os = "fuchsia")]
360    #[fasync::run_singlethreaded(test)]
361    async fn test_clone() {
362        use vfs::file::vmo::read_only;
363
364        // Set up a directory server.
365        let dir = vfs::pseudo_directory! {
366            "foo" => vfs::pseudo_directory! {
367                "bar" => read_only(b"Fuchsia"),
368            },
369        };
370        let client_end = vfs::directory::serve_read_only(dir).into_client_end().unwrap();
371
372        // Make a namespace pointing to that server.
373        let mut namespace = Namespace::new();
374        namespace.add(&ns_path("/data"), client_end).unwrap();
375
376        // Both this namespace and the clone should point to the same server.
377        let namespace_clone = namespace.clone();
378
379        let mut entries = namespace.flatten();
380        let mut entries_clone = namespace_clone.flatten();
381
382        async fn verify(entry: Entry) {
383            assert_eq!(entry.path.to_string(), "/data");
384            let dir = entry.directory.into_proxy();
385            let file = fuchsia_fs::directory::open_file(&dir, "foo/bar", fio::PERM_READABLE)
386                .await
387                .unwrap();
388            let content = fuchsia_fs::file::read(&file).await.unwrap();
389            assert_eq!(content, b"Fuchsia");
390        }
391
392        verify(entries.remove(0)).await;
393        verify(entries_clone.remove(0)).await;
394    }
395
396    #[test]
397    fn test_flatten() {
398        let mut namespace = Namespace::new();
399        let (client_end, server_end) = fidl::endpoints::create_endpoints();
400        namespace.add(&ns_path("/svc"), client_end).unwrap();
401        let mut entries = namespace.flatten();
402        assert_eq!(entries[0].path.to_string(), "/svc");
403        entries
404            .remove(0)
405            .directory
406            .into_channel()
407            .signal_peer(zx::Signals::empty(), zx::Signals::USER_0)
408            .unwrap();
409        server_end
410            .as_handle_ref()
411            .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE)
412            .unwrap();
413    }
414
415    #[test]
416    fn test_remove() {
417        let mut namespace = Namespace::new();
418        let (client_end, server_end) = fidl::endpoints::create_endpoints();
419        namespace.add(&ns_path("/svc"), client_end).unwrap();
420        let client_end = namespace.remove(&ns_path("/svc")).unwrap();
421        let entries = namespace.flatten();
422        assert!(entries.is_empty());
423        client_end.into_channel().signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
424        server_end
425            .as_handle_ref()
426            .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE)
427            .unwrap();
428    }
429}