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        use fidl::AsHandleRef;
92
93        // TODO(https://fxbug.dev/42083023): The unsafe block can go away if Rust FIDL bindings exposed the
94        // feature of calling FIDL methods (e.g. Clone) on a borrowed client endpoint.
95        let tree = self.tree.map_ref(|dir| {
96            let raw_handle = dir.channel().as_handle_ref().raw_handle();
97            // SAFETY: the channel is forgotten at the end of scope so it is not double closed.
98            unsafe {
99                let borrowed: zx::Channel = zx::Handle::from_raw(raw_handle).into();
100                let borrowed = fio::DirectorySynchronousProxy::new(borrowed);
101                let (client_end, server_end) =
102                    fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
103                let _ = borrowed.clone(server_end.into_channel().into());
104                std::mem::forget(borrowed.into_channel());
105                client_end
106            }
107        });
108        Self { tree }
109    }
110}
111
112impl From<Namespace> for Vec<Entry> {
113    fn from(namespace: Namespace) -> Self {
114        namespace.flatten()
115    }
116}
117
118impl From<Namespace> for Vec<fcrunner::ComponentNamespaceEntry> {
119    fn from(namespace: Namespace) -> Self {
120        namespace.flatten().into_iter().map(Into::into).collect()
121    }
122}
123
124impl From<Namespace> for Vec<fprocess::NameInfo> {
125    fn from(namespace: Namespace) -> Self {
126        namespace.flatten().into_iter().map(Into::into).collect()
127    }
128}
129
130#[cfg(target_os = "fuchsia")]
131impl From<Namespace> for Vec<process_builder::NamespaceEntry> {
132    fn from(namespace: Namespace) -> Self {
133        namespace.flatten().into_iter().map(Into::into).collect()
134    }
135}
136
137/// Converts the [Namespace] to a vfs [TreeBuilder] with tree nodes for each entry.
138///
139/// The TreeBuilder can then be used to build a vfs directory for this Namespace.
140#[cfg(target_os = "fuchsia")]
141impl TryFrom<Namespace> for vfs::tree_builder::TreeBuilder {
142    type Error = vfs::tree_builder::Error;
143
144    fn try_from(namespace: Namespace) -> Result<Self, Self::Error> {
145        let mut builder = vfs::tree_builder::TreeBuilder::empty_dir();
146        for Entry { path, directory } in namespace.flatten().into_iter() {
147            let path: Vec<&str> = path.iter_segments().map(|s| s.as_str()).collect();
148            builder.add_entry(path, vfs::remote::remote_dir(directory.into_proxy()))?;
149        }
150        Ok(builder)
151    }
152}
153
154/// Converts the [Namespace] into a vfs directory.
155#[cfg(target_os = "fuchsia")]
156impl TryFrom<Namespace> for Arc<vfs::directory::immutable::simple::Simple> {
157    type Error = vfs::tree_builder::Error;
158
159    fn try_from(namespace: Namespace) -> Result<Self, Self::Error> {
160        let builder: vfs::tree_builder::TreeBuilder = namespace.try_into()?;
161        Ok(builder.build())
162    }
163}
164
165impl From<Tree<ClientEnd<fio::DirectoryMarker>>> for Namespace {
166    fn from(tree: Tree<ClientEnd<fio::DirectoryMarker>>) -> Self {
167        Self { tree }
168    }
169}
170
171impl TryFrom<Vec<Entry>> for Namespace {
172    type Error = NamespaceError;
173
174    fn try_from(value: Vec<Entry>) -> Result<Self, Self::Error> {
175        let mut ns = Namespace::new();
176        for entry in value {
177            ns.add(&entry.path, entry.directory)?;
178        }
179        Ok(ns)
180    }
181}
182
183impl TryFrom<Vec<fcrunner::ComponentNamespaceEntry>> for Namespace {
184    type Error = NamespaceError;
185
186    fn try_from(entries: Vec<fcrunner::ComponentNamespaceEntry>) -> Result<Self, Self::Error> {
187        let entries: Vec<Entry> =
188            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
189        entries.try_into()
190    }
191}
192
193impl TryFrom<Vec<fcomponent::NamespaceEntry>> for Namespace {
194    type Error = NamespaceError;
195
196    fn try_from(entries: Vec<fcomponent::NamespaceEntry>) -> Result<Self, Self::Error> {
197        let entries: Vec<Entry> =
198            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
199        entries.try_into()
200    }
201}
202
203impl TryFrom<Vec<fprocess::NameInfo>> for Namespace {
204    type Error = NamespaceError;
205
206    fn try_from(entries: Vec<fprocess::NameInfo>) -> Result<Self, Self::Error> {
207        let entries: Vec<Entry> =
208            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
209        entries.try_into()
210    }
211}
212
213#[cfg(target_os = "fuchsia")]
214impl TryFrom<Vec<process_builder::NamespaceEntry>> for Namespace {
215    type Error = NamespaceError;
216
217    fn try_from(entries: Vec<process_builder::NamespaceEntry>) -> Result<Self, Self::Error> {
218        let entries: Vec<Entry> =
219            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
220        entries.try_into()
221    }
222}
223
224/// A container for a single namespace entry, containing a path and a directory handle.
225#[derive(Eq, Ord, PartialOrd, PartialEq, Debug)]
226pub struct Entry {
227    /// Namespace path.
228    pub path: NamespacePath,
229
230    /// Namespace directory handle.
231    pub directory: ClientEnd<fio::DirectoryMarker>,
232}
233
234impl From<Entry> for fcrunner::ComponentNamespaceEntry {
235    fn from(entry: Entry) -> Self {
236        Self {
237            path: Some(entry.path.into()),
238            directory: Some(entry.directory),
239            ..Default::default()
240        }
241    }
242}
243
244impl From<Entry> for fcomponent::NamespaceEntry {
245    fn from(entry: Entry) -> Self {
246        Self {
247            path: Some(entry.path.into()),
248            directory: Some(entry.directory),
249            ..Default::default()
250        }
251    }
252}
253
254impl From<Entry> for fprocess::NameInfo {
255    fn from(entry: Entry) -> Self {
256        Self { path: entry.path.into(), directory: entry.directory }
257    }
258}
259
260#[cfg(target_os = "fuchsia")]
261impl From<Entry> for process_builder::NamespaceEntry {
262    fn from(entry: Entry) -> Self {
263        Self { path: entry.path.into(), directory: entry.directory }
264    }
265}
266
267#[derive(Debug, Clone, Error)]
268pub enum EntryError {
269    #[error("path is not set")]
270    MissingPath,
271
272    #[error("directory is not set")]
273    MissingDirectory,
274
275    #[error("entry type is not supported (must be directory or dictionary")]
276    UnsupportedType,
277
278    #[error("path is invalid for a namespace entry: `{0}`")]
279    InvalidPath(#[from] cm_types::ParseError),
280}
281
282impl TryFrom<fcrunner::ComponentNamespaceEntry> for Entry {
283    type Error = EntryError;
284
285    fn try_from(entry: fcrunner::ComponentNamespaceEntry) -> Result<Self, Self::Error> {
286        Ok(Self {
287            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
288            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
289        })
290    }
291}
292
293impl TryFrom<fcomponent::NamespaceEntry> for Entry {
294    type Error = EntryError;
295
296    fn try_from(entry: fcomponent::NamespaceEntry) -> Result<Self, Self::Error> {
297        Ok(Self {
298            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
299            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
300        })
301    }
302}
303
304impl TryFrom<fprocess::NameInfo> for Entry {
305    type Error = EntryError;
306
307    fn try_from(entry: fprocess::NameInfo) -> Result<Self, Self::Error> {
308        Ok(Self { path: entry.path.parse()?, directory: entry.directory })
309    }
310}
311
312#[cfg(target_os = "fuchsia")]
313impl TryFrom<process_builder::NamespaceEntry> for Entry {
314    type Error = EntryError;
315
316    fn try_from(entry: process_builder::NamespaceEntry) -> Result<Self, Self::Error> {
317        Ok(Self { path: entry.path.try_into()?, directory: entry.directory })
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use assert_matches::assert_matches;
325    use fidl::endpoints::Proxy as _;
326    use fuchsia_async as fasync;
327    use zx::{AsHandleRef, Peered};
328
329    fn ns_path(str: &str) -> NamespacePath {
330        str.parse().unwrap()
331    }
332
333    #[test]
334    fn test_try_from_namespace() {
335        {
336            let (client_end_1, _) = fidl::endpoints::create_endpoints();
337            let (client_end_2, _) = fidl::endpoints::create_endpoints();
338            let entries = vec![
339                Entry { path: ns_path("/foo"), directory: client_end_1 },
340                Entry { path: ns_path("/foo/bar"), directory: client_end_2 },
341            ];
342            assert_matches!(
343                Namespace::try_from(entries),
344                Err(NamespaceError::Shadow(path)) if path.to_string() == "/foo/bar"
345            );
346        }
347        {
348            let (client_end_1, _) = fidl::endpoints::create_endpoints();
349            let (client_end_2, _) = fidl::endpoints::create_endpoints();
350            let entries = vec![
351                Entry { path: ns_path("/foo"), directory: client_end_1 },
352                Entry { path: ns_path("/foo"), directory: client_end_2 },
353            ];
354            assert_matches!(
355                Namespace::try_from(entries),
356                Err(NamespaceError::Duplicate(path)) if path.to_string() == "/foo"
357            );
358        }
359    }
360
361    #[cfg(target_os = "fuchsia")]
362    #[fasync::run_singlethreaded(test)]
363    async fn test_clone() {
364        use vfs::file::vmo::read_only;
365
366        // Set up a directory server.
367        let dir = vfs::pseudo_directory! {
368            "foo" => vfs::pseudo_directory! {
369                "bar" => read_only(b"Fuchsia"),
370            },
371        };
372        let client_end = vfs::directory::serve_read_only(dir).into_client_end().unwrap();
373
374        // Make a namespace pointing to that server.
375        let mut namespace = Namespace::new();
376        namespace.add(&ns_path("/data"), client_end).unwrap();
377
378        // Both this namespace and the clone should point to the same server.
379        let namespace_clone = namespace.clone();
380
381        let mut entries = namespace.flatten();
382        let mut entries_clone = namespace_clone.flatten();
383
384        async fn verify(entry: Entry) {
385            assert_eq!(entry.path.to_string(), "/data");
386            let dir = entry.directory.into_proxy();
387            let file = fuchsia_fs::directory::open_file(&dir, "foo/bar", fio::PERM_READABLE)
388                .await
389                .unwrap();
390            let content = fuchsia_fs::file::read(&file).await.unwrap();
391            assert_eq!(content, b"Fuchsia");
392        }
393
394        verify(entries.remove(0)).await;
395        verify(entries_clone.remove(0)).await;
396    }
397
398    #[test]
399    fn test_flatten() {
400        let mut namespace = Namespace::new();
401        let (client_end, server_end) = fidl::endpoints::create_endpoints();
402        namespace.add(&ns_path("/svc"), client_end).unwrap();
403        let mut entries = namespace.flatten();
404        assert_eq!(entries[0].path.to_string(), "/svc");
405        entries
406            .remove(0)
407            .directory
408            .into_channel()
409            .signal_peer(zx::Signals::empty(), zx::Signals::USER_0)
410            .unwrap();
411        server_end.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE).unwrap();
412    }
413
414    #[test]
415    fn test_remove() {
416        let mut namespace = Namespace::new();
417        let (client_end, server_end) = fidl::endpoints::create_endpoints();
418        namespace.add(&ns_path("/svc"), client_end).unwrap();
419        let client_end = namespace.remove(&ns_path("/svc")).unwrap();
420        let entries = namespace.flatten();
421        assert!(entries.is_empty());
422        client_end.into_channel().signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
423        server_end.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE).unwrap();
424    }
425}