Skip to main content

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<cm_types::NamespaceEntry> for Entry {
164    fn from(entry: cm_types::NamespaceEntry) -> Self {
165        Entry { path: entry.path, directory: entry.directory }
166    }
167}
168
169impl From<Tree<ClientEnd<fio::DirectoryMarker>>> for Namespace {
170    fn from(tree: Tree<ClientEnd<fio::DirectoryMarker>>) -> Self {
171        Self { tree }
172    }
173}
174
175impl TryFrom<Vec<cm_types::NamespaceEntry>> for Namespace {
176    type Error = NamespaceError;
177
178    fn try_from(value: Vec<cm_types::NamespaceEntry>) -> Result<Self, Self::Error> {
179        let mut ns = Namespace::new();
180        for entry in value {
181            ns.add(&entry.path, entry.directory)?;
182        }
183        Ok(ns)
184    }
185}
186
187impl TryFrom<Vec<Entry>> for Namespace {
188    type Error = NamespaceError;
189
190    fn try_from(value: Vec<Entry>) -> Result<Self, Self::Error> {
191        let mut ns = Namespace::new();
192        for entry in value {
193            ns.add(&entry.path, entry.directory)?;
194        }
195        Ok(ns)
196    }
197}
198
199impl TryFrom<Vec<fcrunner::ComponentNamespaceEntry>> for Namespace {
200    type Error = NamespaceError;
201
202    fn try_from(entries: Vec<fcrunner::ComponentNamespaceEntry>) -> Result<Self, Self::Error> {
203        let entries: Vec<Entry> =
204            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
205        entries.try_into()
206    }
207}
208
209impl TryFrom<Vec<fcomponent::NamespaceEntry>> for Namespace {
210    type Error = NamespaceError;
211
212    fn try_from(entries: Vec<fcomponent::NamespaceEntry>) -> Result<Self, Self::Error> {
213        let entries: Vec<Entry> =
214            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
215        entries.try_into()
216    }
217}
218
219impl TryFrom<Vec<fprocess::NameInfo>> for Namespace {
220    type Error = NamespaceError;
221
222    fn try_from(entries: Vec<fprocess::NameInfo>) -> Result<Self, Self::Error> {
223        let entries: Vec<Entry> =
224            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
225        entries.try_into()
226    }
227}
228
229#[cfg(target_os = "fuchsia")]
230impl TryFrom<Vec<process_builder::NamespaceEntry>> for Namespace {
231    type Error = NamespaceError;
232
233    fn try_from(entries: Vec<process_builder::NamespaceEntry>) -> Result<Self, Self::Error> {
234        let entries: Vec<Entry> =
235            entries.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, EntryError>>()?;
236        entries.try_into()
237    }
238}
239
240/// A container for a single namespace entry, containing a path and a directory handle.
241#[derive(Eq, Ord, PartialOrd, PartialEq, Debug)]
242pub struct Entry {
243    /// Namespace path.
244    pub path: NamespacePath,
245
246    /// Namespace directory handle.
247    pub directory: ClientEnd<fio::DirectoryMarker>,
248}
249
250impl From<Entry> for fcrunner::ComponentNamespaceEntry {
251    fn from(entry: Entry) -> Self {
252        Self {
253            path: Some(entry.path.into()),
254            directory: Some(entry.directory),
255            ..Default::default()
256        }
257    }
258}
259
260impl From<Entry> for fcomponent::NamespaceEntry {
261    fn from(entry: Entry) -> Self {
262        Self {
263            path: Some(entry.path.into()),
264            directory: Some(entry.directory),
265            ..Default::default()
266        }
267    }
268}
269
270impl From<Entry> for fprocess::NameInfo {
271    fn from(entry: Entry) -> Self {
272        Self { path: entry.path.into(), directory: entry.directory }
273    }
274}
275
276#[cfg(target_os = "fuchsia")]
277impl From<Entry> for process_builder::NamespaceEntry {
278    fn from(entry: Entry) -> Self {
279        Self { path: entry.path.into(), directory: entry.directory }
280    }
281}
282
283#[derive(Debug, Clone, Error)]
284pub enum EntryError {
285    #[error("path is not set")]
286    MissingPath,
287
288    #[error("directory is not set")]
289    MissingDirectory,
290
291    #[error("entry type is not supported (must be directory or dictionary")]
292    UnsupportedType,
293
294    #[error("path is invalid for a namespace entry: `{0}`")]
295    InvalidPath(#[from] cm_types::ParseError),
296}
297
298impl TryFrom<fcrunner::ComponentNamespaceEntry> for Entry {
299    type Error = EntryError;
300
301    fn try_from(entry: fcrunner::ComponentNamespaceEntry) -> Result<Self, Self::Error> {
302        Ok(Self {
303            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
304            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
305        })
306    }
307}
308
309impl TryFrom<fcomponent::NamespaceEntry> for Entry {
310    type Error = EntryError;
311
312    fn try_from(entry: fcomponent::NamespaceEntry) -> Result<Self, Self::Error> {
313        Ok(Self {
314            path: entry.path.ok_or(EntryError::MissingPath)?.parse()?,
315            directory: entry.directory.ok_or(EntryError::MissingDirectory)?,
316        })
317    }
318}
319
320impl TryFrom<fprocess::NameInfo> for Entry {
321    type Error = EntryError;
322
323    fn try_from(entry: fprocess::NameInfo) -> Result<Self, Self::Error> {
324        Ok(Self { path: entry.path.parse()?, directory: entry.directory })
325    }
326}
327
328#[cfg(target_os = "fuchsia")]
329impl TryFrom<process_builder::NamespaceEntry> for Entry {
330    type Error = EntryError;
331
332    fn try_from(entry: process_builder::NamespaceEntry) -> Result<Self, Self::Error> {
333        Ok(Self { path: entry.path.try_into()?, directory: entry.directory })
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use assert_matches::assert_matches;
341    use fidl::endpoints::Proxy as _;
342    use fuchsia_async as fasync;
343    use zx::{AsHandleRef, Peered};
344
345    fn ns_path(str: &str) -> NamespacePath {
346        str.parse().unwrap()
347    }
348
349    #[test]
350    fn test_try_from_namespace() {
351        {
352            let (client_end_1, _) = fidl::endpoints::create_endpoints();
353            let (client_end_2, _) = fidl::endpoints::create_endpoints();
354            let entries = vec![
355                Entry { path: ns_path("/foo"), directory: client_end_1 },
356                Entry { path: ns_path("/foo/bar"), directory: client_end_2 },
357            ];
358            assert_matches!(
359                Namespace::try_from(entries),
360                Err(NamespaceError::Shadow(path)) if path.to_string() == "/foo/bar"
361            );
362        }
363        {
364            let (client_end_1, _) = fidl::endpoints::create_endpoints();
365            let (client_end_2, _) = fidl::endpoints::create_endpoints();
366            let entries = vec![
367                Entry { path: ns_path("/foo"), directory: client_end_1 },
368                Entry { path: ns_path("/foo"), directory: client_end_2 },
369            ];
370            assert_matches!(
371                Namespace::try_from(entries),
372                Err(NamespaceError::Duplicate(path)) if path.to_string() == "/foo"
373            );
374        }
375    }
376
377    #[cfg(target_os = "fuchsia")]
378    #[fasync::run_singlethreaded(test)]
379    async fn test_clone() {
380        use vfs::file::vmo::read_only;
381
382        // Set up a directory server.
383        let dir = vfs::pseudo_directory! {
384            "foo" => vfs::pseudo_directory! {
385                "bar" => read_only(b"Fuchsia"),
386            },
387        };
388        let client_end = vfs::directory::serve_read_only(dir).into_client_end().unwrap();
389
390        // Make a namespace pointing to that server.
391        let mut namespace = Namespace::new();
392        namespace.add(&ns_path("/data"), client_end).unwrap();
393
394        // Both this namespace and the clone should point to the same server.
395        let namespace_clone = namespace.clone();
396
397        let mut entries = namespace.flatten();
398        let mut entries_clone = namespace_clone.flatten();
399
400        async fn verify(entry: Entry) {
401            assert_eq!(entry.path.to_string(), "/data");
402            let dir = entry.directory.into_proxy();
403            let file = fuchsia_fs::directory::open_file(&dir, "foo/bar", fio::PERM_READABLE)
404                .await
405                .unwrap();
406            let content = fuchsia_fs::file::read(&file).await.unwrap();
407            assert_eq!(content, b"Fuchsia");
408        }
409
410        verify(entries.remove(0)).await;
411        verify(entries_clone.remove(0)).await;
412    }
413
414    #[test]
415    fn test_flatten() {
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 mut entries = namespace.flatten();
420        assert_eq!(entries[0].path.to_string(), "/svc");
421        entries
422            .remove(0)
423            .directory
424            .into_channel()
425            .signal_peer(zx::Signals::empty(), zx::Signals::USER_0)
426            .unwrap();
427        server_end
428            .as_handle_ref()
429            .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE)
430            .unwrap();
431    }
432
433    #[test]
434    fn test_remove() {
435        let mut namespace = Namespace::new();
436        let (client_end, server_end) = fidl::endpoints::create_endpoints();
437        namespace.add(&ns_path("/svc"), client_end).unwrap();
438        let client_end = namespace.remove(&ns_path("/svc")).unwrap();
439        let entries = namespace.flatten();
440        assert!(entries.is_empty());
441        client_end.into_channel().signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
442        server_end
443            .as_handle_ref()
444            .wait_one(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE)
445            .unwrap();
446    }
447}