Skip to main content

fuchsia_fs_fdomain/
node.rs

1// Copyright 2020 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//! Utility functions for fuchsia.io nodes.
6
7use flex_fuchsia_io as fio;
8use futures::prelude::*;
9use thiserror::Error;
10
11#[cfg(target_os = "fuchsia")]
12#[cfg(not(feature = "fdomain"))]
13pub use fuchsia::*;
14
15#[cfg(target_os = "fuchsia")]
16#[cfg(not(feature = "fdomain"))]
17mod fuchsia {
18    use super::*;
19
20    /// Opens the given `path` from the current namespace as a [`NodeProxy`].
21    ///
22    /// The target is assumed to implement fuchsia.io.Node but this isn't verified. To connect to a
23    /// filesystem node which doesn't implement fuchsia.io.Node, use the functions in
24    /// [`fuchsia_component::client`] instead.
25    ///
26    /// If the namespace path doesn't exist, or we fail to make the channel pair, this returns an
27    /// error. However, if incorrect flags are sent, or if the rest of the path sent to the
28    /// filesystem server doesn't exist, this will still return success. Instead, the returned
29    /// NodeProxy channel pair will be closed with an epitaph.
30    pub fn open_in_namespace(path: &str, flags: fio::Flags) -> Result<fio::NodeProxy, OpenError> {
31        let (node, request) = fidl::endpoints::create_proxy();
32        open_channel_in_namespace(path, flags, request)?;
33        Ok(node)
34    }
35
36    /// Asynchronously opens the given [`path`] in the current namespace, serving the connection
37    /// over [`request`]. Once the channel is connected, any calls made prior are serviced.
38    ///
39    /// The target is assumed to implement fuchsia.io.Node but this isn't verified. To connect to a
40    /// filesystem node which doesn't implement fuchsia.io.Node, use the functions in
41    /// [`fuchsia_component::client`] instead.
42    ///
43    /// If the namespace path doesn't exist, this returns an error. However, if incorrect flags are
44    /// sent, or if the rest of the path sent to the filesystem server doesn't exist, this will
45    /// still return success. Instead, the [`request`] channel will be closed with an epitaph.
46    pub fn open_channel_in_namespace(
47        path: &str,
48        flags: fio::Flags,
49        request: fidl::endpoints::ServerEnd<fio::NodeMarker>,
50    ) -> Result<(), OpenError> {
51        let namespace = fdio::Namespace::installed().map_err(OpenError::Namespace)?;
52        namespace.open(path, flags, request.into_channel()).map_err(OpenError::Namespace)
53    }
54}
55
56/// An error encountered while opening a node
57#[derive(Debug, Clone, Error)]
58#[allow(missing_docs)]
59pub enum OpenError {
60    #[error("while making a fidl proxy: {0}")]
61    CreateProxy(#[source] flex_client::Error),
62
63    #[error("while opening from namespace: {0}")]
64    Namespace(#[source] zx_status::Status),
65
66    #[error("while sending open request: {0}")]
67    SendOpenRequest(#[source] fidl::Error),
68
69    #[error("node event stream closed prematurely")]
70    OnOpenEventStreamClosed,
71
72    #[error("while reading OnOpen event: {0}")]
73    OnOpenDecode(#[source] fidl::Error),
74
75    #[error("open failed with status: {0}")]
76    OpenError(#[source] zx_status::Status),
77
78    #[error("remote responded with success but provided no node info")]
79    MissingOnOpenInfo,
80
81    #[error("expected node to be a {expected:?}, but got a {actual:?}")]
82    UnexpectedNodeKind { expected: Kind, actual: Kind },
83
84    #[error("received unknown event (ordinal = {ordinal})")]
85    UnknownEvent { ordinal: u64 },
86}
87
88impl OpenError {
89    /// Returns true if the open failed because the node was not found.
90    pub fn is_not_found_error(&self) -> bool {
91        matches!(
92            self,
93            OpenError::OpenError(zx_status::Status::NOT_FOUND)
94                | OpenError::Namespace(zx_status::Status::NOT_FOUND)
95        )
96    }
97}
98
99/// An error encountered while cloning a node
100#[derive(Debug, Clone, Error)]
101#[allow(missing_docs)]
102pub enum CloneError {
103    #[error("while making a fidl proxy: {0}")]
104    CreateProxy(#[source] fidl::Error),
105
106    #[error("while sending clone request: {0}")]
107    SendCloneRequest(#[source] fidl::Error),
108}
109
110/// An error encountered while closing a node
111#[derive(Debug, Clone, Error)]
112#[allow(missing_docs)]
113pub enum CloseError {
114    #[error("while sending close request: {0}")]
115    SendCloseRequest(#[source] fidl::Error),
116
117    #[error("close failed with status: {0}")]
118    CloseError(#[source] zx_status::Status),
119}
120
121/// An error encountered while renaming a node
122#[derive(Debug, Clone, Error)]
123#[allow(missing_docs)]
124pub enum RenameError {
125    #[error("while sending rename request")]
126    SendRenameRequest(#[source] fidl::Error),
127
128    #[error("while sending get_token request")]
129    SendGetTokenRequest(#[source] fidl::Error),
130
131    #[error("rename failed with status")]
132    RenameError(#[source] zx_status::Status),
133
134    #[error("while opening subdirectory")]
135    OpenError(#[from] OpenError),
136
137    #[error("get_token failed with status")]
138    GetTokenError(#[source] zx_status::Status),
139
140    #[error("no handle from get token")]
141    NoHandleError,
142}
143
144/// The type of a filesystem node
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[allow(missing_docs)]
147pub enum Kind {
148    Service,
149    File,
150    Directory,
151    Symlink,
152    Unknown,
153}
154
155impl Kind {
156    #[cfg(any(fuchsia_api_level_at_least = "PLATFORM", not(fuchsia_api_level_at_least = "NEXT")))]
157    pub(crate) fn kind_of(info: &fio::NodeInfoDeprecated) -> Kind {
158        match info {
159            fio::NodeInfoDeprecated::Service(_) => Kind::Service,
160            fio::NodeInfoDeprecated::File(_) => Kind::File,
161            fio::NodeInfoDeprecated::Directory(_) => Kind::Directory,
162            fio::NodeInfoDeprecated::Symlink(_) => Kind::Symlink,
163        }
164    }
165
166    #[cfg(any(fuchsia_api_level_at_least = "PLATFORM", not(fuchsia_api_level_at_least = "NEXT")))]
167    fn expect_file(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
168        match info {
169            fio::NodeInfoDeprecated::File(fio::FileObject { .. }) => Ok(()),
170            other => Err(Kind::kind_of(&other)),
171        }
172    }
173
174    #[cfg(any(fuchsia_api_level_at_least = "PLATFORM", not(fuchsia_api_level_at_least = "NEXT")))]
175    fn expect_directory(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
176        match info {
177            fio::NodeInfoDeprecated::Directory(fio::DirectoryObject) => Ok(()),
178            other => Err(Kind::kind_of(&other)),
179        }
180    }
181
182    pub(crate) fn kind_of2(representation: &fio::Representation) -> Kind {
183        match representation {
184            fio::Representation::Directory(_) => Kind::Directory,
185            fio::Representation::File(_) => Kind::File,
186            fio::Representation::Symlink(_) => Kind::Symlink,
187            _ => Kind::Unknown,
188        }
189    }
190
191    fn expect_file2(representation: &fio::Representation) -> Result<(), Kind> {
192        match representation {
193            fio::Representation::File(fio::FileInfo { .. }) => Ok(()),
194            other => Err(Kind::kind_of2(other)),
195        }
196    }
197
198    fn expect_directory2(representation: &fio::Representation) -> Result<(), Kind> {
199        match representation {
200            fio::Representation::Directory(_) => Ok(()),
201            other => Err(Kind::kind_of2(other)),
202        }
203    }
204}
205
206/// Gracefully closes the node proxy from the remote end.
207pub async fn close(node: fio::NodeProxy) -> Result<(), CloseError> {
208    let result = node.close().await.map_err(CloseError::SendCloseRequest)?;
209    result.map_err(|s| CloseError::CloseError(zx_status::Status::from_raw(s)))
210}
211
212/// Consume the first event from this NodeProxy's event stream, returning the proxy if it is
213/// the expected type or an error otherwise.
214pub(crate) async fn verify_node_describe_event(
215    node: fio::NodeProxy,
216) -> Result<fio::NodeProxy, OpenError> {
217    match take_on_open_event(&node).await? {
218        #[cfg(any(
219            fuchsia_api_level_at_least = "PLATFORM",
220            not(fuchsia_api_level_at_least = "NEXT")
221        ))]
222        fio::NodeEvent::OnOpen_ { s: status, info } => {
223            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
224            info.ok_or(OpenError::MissingOnOpenInfo)?;
225        }
226        fio::NodeEvent::OnRepresentation { .. } => {}
227        fio::NodeEvent::_UnknownEvent { ordinal, .. } => {
228            return Err(OpenError::UnknownEvent { ordinal });
229        }
230    }
231
232    Ok(node)
233}
234
235/// Consume the first event from this DirectoryProxy's event stream, returning the proxy if it is
236/// the expected type or an error otherwise.
237pub(crate) async fn verify_directory_describe_event(
238    node: fio::DirectoryProxy,
239) -> Result<fio::DirectoryProxy, OpenError> {
240    match take_on_open_event(&node).await? {
241        #[cfg(any(
242            fuchsia_api_level_at_least = "PLATFORM",
243            not(fuchsia_api_level_at_least = "NEXT")
244        ))]
245        fio::DirectoryEvent::OnOpen_ { s: status, info } => {
246            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
247            let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
248            let () = Kind::expect_directory(*info).map_err(|actual| {
249                OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
250            })?;
251        }
252        fio::DirectoryEvent::OnRepresentation { payload } => {
253            let () = Kind::expect_directory2(&payload).map_err(|actual| {
254                OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
255            })?;
256        }
257        fio::DirectoryEvent::_UnknownEvent { ordinal, .. } => {
258            return Err(OpenError::UnknownEvent { ordinal });
259        }
260    }
261
262    Ok(node)
263}
264
265/// Consume the first event from this FileProxy's event stream, returning the proxy if it is the
266/// expected type or an error otherwise.
267pub(crate) async fn verify_file_describe_event(
268    node: fio::FileProxy,
269) -> Result<fio::FileProxy, OpenError> {
270    match take_on_open_event(&node).await? {
271        #[cfg(any(
272            fuchsia_api_level_at_least = "PLATFORM",
273            not(fuchsia_api_level_at_least = "NEXT")
274        ))]
275        fio::FileEvent::OnOpen_ { s: status, info } => {
276            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
277            let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
278            let () = Kind::expect_file(*info)
279                .map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
280        }
281        fio::FileEvent::OnRepresentation { payload } => {
282            let () = Kind::expect_file2(&payload)
283                .map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
284        }
285        fio::FileEvent::_UnknownEvent { ordinal, .. } => {
286            return Err(OpenError::UnknownEvent { ordinal });
287        }
288    }
289
290    Ok(node)
291}
292
293pub(crate) trait OnOpenEventProducer {
294    type Event;
295    type Stream: futures::Stream<Item = Result<Self::Event, fidl::Error>> + Unpin;
296    fn take_event_stream(&self) -> Self::Stream;
297}
298
299macro_rules! impl_on_open_event_producer {
300    ($proxy:ty, $event:ty, $stream:ty) => {
301        impl OnOpenEventProducer for $proxy {
302            type Event = $event;
303            type Stream = $stream;
304            fn take_event_stream(&self) -> Self::Stream {
305                self.take_event_stream()
306            }
307        }
308    };
309}
310
311impl_on_open_event_producer!(fio::NodeProxy, fio::NodeEvent, fio::NodeEventStream);
312impl_on_open_event_producer!(fio::FileProxy, fio::FileEvent, fio::FileEventStream);
313impl_on_open_event_producer!(fio::DirectoryProxy, fio::DirectoryEvent, fio::DirectoryEventStream);
314
315pub(crate) async fn take_on_open_event<T>(node: &T) -> Result<T::Event, OpenError>
316where
317    T: OnOpenEventProducer,
318{
319    node.take_event_stream().next().await.ok_or(OpenError::OnOpenEventStreamClosed)?.map_err(|e| {
320        if let fidl::Error::ClientChannelClosed { status, .. } = e {
321            OpenError::OpenError(status)
322        } else {
323            OpenError::OnOpenDecode(e)
324        }
325    })
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use assert_matches::assert_matches;
332    use fidl::endpoints::DiscoverableProtocolMarker as _;
333    use fuchsia_async as fasync;
334
335    // open_in_namespace
336
337    #[fasync::run_singlethreaded(test)]
338    async fn open_in_namespace_opens_real_node() {
339        let file_node = open_in_namespace("/pkg/data/file", fio::PERM_READABLE).unwrap();
340        let protocol = file_node.query().await.unwrap();
341        assert_eq!(protocol, fio::FileMarker::PROTOCOL_NAME.as_bytes());
342
343        let dir_node = open_in_namespace("/pkg/data", fio::PERM_READABLE).unwrap();
344        let protocol = dir_node.query().await.unwrap();
345        assert_eq!(protocol, fio::DirectoryMarker::PROTOCOL_NAME.as_bytes());
346    }
347
348    #[fasync::run_singlethreaded(test)]
349    async fn open_in_namespace_opens_fake_node_under_of_root_namespace_entry() {
350        let notfound = open_in_namespace("/pkg/fake", fio::PERM_READABLE).unwrap();
351        // The open error is not detected until the proxy is interacted with.
352        assert_matches!(close(notfound).await, Err(_));
353    }
354
355    #[fasync::run_singlethreaded(test)]
356    async fn open_in_namespace_rejects_fake_root_namespace_entry() {
357        assert_matches!(
358            open_in_namespace("/fake", fio::PERM_READABLE),
359            Err(OpenError::Namespace(zx_status::Status::NOT_FOUND))
360        );
361    }
362}