fuchsia_fs/
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    pub(crate) fn kind_of(info: &fio::NodeInfoDeprecated) -> Kind {
157        match info {
158            fio::NodeInfoDeprecated::Service(_) => Kind::Service,
159            fio::NodeInfoDeprecated::File(_) => Kind::File,
160            fio::NodeInfoDeprecated::Directory(_) => Kind::Directory,
161            fio::NodeInfoDeprecated::Symlink(_) => Kind::Symlink,
162        }
163    }
164
165    fn expect_file(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
166        match info {
167            fio::NodeInfoDeprecated::File(fio::FileObject { .. }) => Ok(()),
168            other => Err(Kind::kind_of(&other)),
169        }
170    }
171
172    fn expect_directory(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
173        match info {
174            fio::NodeInfoDeprecated::Directory(fio::DirectoryObject) => Ok(()),
175            other => Err(Kind::kind_of(&other)),
176        }
177    }
178
179    pub(crate) fn kind_of2(representation: &fio::Representation) -> Kind {
180        match representation {
181            fio::Representation::Directory(_) => Kind::Directory,
182            fio::Representation::File(_) => Kind::File,
183            fio::Representation::Symlink(_) => Kind::Symlink,
184            _ => Kind::Unknown,
185        }
186    }
187
188    fn expect_file2(representation: &fio::Representation) -> Result<(), Kind> {
189        match representation {
190            fio::Representation::File(fio::FileInfo { .. }) => Ok(()),
191            other => Err(Kind::kind_of2(other)),
192        }
193    }
194
195    fn expect_directory2(representation: &fio::Representation) -> Result<(), Kind> {
196        match representation {
197            fio::Representation::Directory(_) => Ok(()),
198            other => Err(Kind::kind_of2(other)),
199        }
200    }
201}
202
203/// Gracefully closes the node proxy from the remote end.
204pub async fn close(node: fio::NodeProxy) -> Result<(), CloseError> {
205    let result = node.close().await.map_err(CloseError::SendCloseRequest)?;
206    result.map_err(|s| CloseError::CloseError(zx_status::Status::from_raw(s)))
207}
208
209/// Consume the first event from this NodeProxy's event stream, returning the proxy if it is
210/// the expected type or an error otherwise.
211pub(crate) async fn verify_node_describe_event(
212    node: fio::NodeProxy,
213) -> Result<fio::NodeProxy, OpenError> {
214    match take_on_open_event(&node).await? {
215        fio::NodeEvent::OnOpen_ { s: status, info } => {
216            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
217            info.ok_or(OpenError::MissingOnOpenInfo)?;
218        }
219        fio::NodeEvent::OnRepresentation { .. } => {}
220        fio::NodeEvent::_UnknownEvent { ordinal, .. } => {
221            return Err(OpenError::UnknownEvent { ordinal })
222        }
223    }
224
225    Ok(node)
226}
227
228/// Consume the first event from this DirectoryProxy's event stream, returning the proxy if it is
229/// the expected type or an error otherwise.
230pub(crate) async fn verify_directory_describe_event(
231    node: fio::DirectoryProxy,
232) -> Result<fio::DirectoryProxy, OpenError> {
233    match take_on_open_event(&node).await? {
234        fio::DirectoryEvent::OnOpen_ { s: status, info } => {
235            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
236            let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
237            let () = Kind::expect_directory(*info).map_err(|actual| {
238                OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
239            })?;
240        }
241        fio::DirectoryEvent::OnRepresentation { payload } => {
242            let () = Kind::expect_directory2(&payload).map_err(|actual| {
243                OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
244            })?;
245        }
246        fio::DirectoryEvent::_UnknownEvent { ordinal, .. } => {
247            return Err(OpenError::UnknownEvent { ordinal })
248        }
249    }
250
251    Ok(node)
252}
253
254/// Consume the first event from this FileProxy's event stream, returning the proxy if it is the
255/// expected type or an error otherwise.
256pub(crate) async fn verify_file_describe_event(
257    node: fio::FileProxy,
258) -> Result<fio::FileProxy, OpenError> {
259    match take_on_open_event(&node).await? {
260        fio::FileEvent::OnOpen_ { s: status, info } => {
261            let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
262            let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
263            let () = Kind::expect_file(*info)
264                .map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
265        }
266        fio::FileEvent::OnRepresentation { payload } => {
267            let () = Kind::expect_file2(&payload)
268                .map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
269        }
270        fio::FileEvent::_UnknownEvent { ordinal, .. } => {
271            return Err(OpenError::UnknownEvent { ordinal })
272        }
273    }
274
275    Ok(node)
276}
277
278pub(crate) trait OnOpenEventProducer {
279    type Event;
280    type Stream: futures::Stream<Item = Result<Self::Event, fidl::Error>> + Unpin;
281    fn take_event_stream(&self) -> Self::Stream;
282}
283
284macro_rules! impl_on_open_event_producer {
285    ($proxy:ty, $event:ty, $stream:ty) => {
286        impl OnOpenEventProducer for $proxy {
287            type Event = $event;
288            type Stream = $stream;
289            fn take_event_stream(&self) -> Self::Stream {
290                self.take_event_stream()
291            }
292        }
293    };
294}
295
296impl_on_open_event_producer!(fio::NodeProxy, fio::NodeEvent, fio::NodeEventStream);
297impl_on_open_event_producer!(fio::FileProxy, fio::FileEvent, fio::FileEventStream);
298impl_on_open_event_producer!(fio::DirectoryProxy, fio::DirectoryEvent, fio::DirectoryEventStream);
299
300pub(crate) async fn take_on_open_event<T>(node: &T) -> Result<T::Event, OpenError>
301where
302    T: OnOpenEventProducer,
303{
304    node.take_event_stream().next().await.ok_or(OpenError::OnOpenEventStreamClosed)?.map_err(|e| {
305        if let fidl::Error::ClientChannelClosed { status, .. } = e {
306            OpenError::OpenError(status)
307        } else {
308            OpenError::OnOpenDecode(e)
309        }
310    })
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use assert_matches::assert_matches;
317    use fuchsia_async as fasync;
318
319    // open_in_namespace
320
321    #[fasync::run_singlethreaded(test)]
322    async fn open_in_namespace_opens_real_node() {
323        let file_node = open_in_namespace("/pkg/data/file", fio::PERM_READABLE).unwrap();
324        let protocol = file_node.query().await.unwrap();
325        assert_eq!(protocol, fio::FILE_PROTOCOL_NAME.as_bytes());
326
327        let dir_node = open_in_namespace("/pkg/data", fio::PERM_READABLE).unwrap();
328        let protocol = dir_node.query().await.unwrap();
329        assert_eq!(protocol, fio::DIRECTORY_PROTOCOL_NAME.as_bytes());
330    }
331
332    #[fasync::run_singlethreaded(test)]
333    async fn open_in_namespace_opens_fake_node_under_of_root_namespace_entry() {
334        let notfound = open_in_namespace("/pkg/fake", fio::PERM_READABLE).unwrap();
335        // The open error is not detected until the proxy is interacted with.
336        assert_matches!(close(notfound).await, Err(_));
337    }
338
339    #[fasync::run_singlethreaded(test)]
340    async fn open_in_namespace_rejects_fake_root_namespace_entry() {
341        assert_matches!(
342            open_in_namespace("/fake", fio::PERM_READABLE),
343            Err(OpenError::Namespace(zx_status::Status::NOT_FOUND))
344        );
345    }
346}