blob: a6fbc2c580c96517e2cbbd369514a98f5ad9fc60 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Utility functions for fuchsia.io nodes.
use {
fidl_fuchsia_io as fio, fuchsia_zircon_status as zx_status, futures::prelude::*,
thiserror::Error,
};
/// An error encountered while opening a node
#[derive(Debug, Clone, Error)]
#[allow(missing_docs)]
pub enum OpenError {
#[error("while making a fidl proxy: {0}")]
CreateProxy(#[source] fidl::Error),
#[error("while opening from namespace: {0}")]
Namespace(#[source] zx_status::Status),
#[error("while sending open request: {0}")]
SendOpenRequest(#[source] fidl::Error),
#[error("node event stream closed prematurely")]
OnOpenEventStreamClosed,
#[error("while reading OnOpen event: {0}")]
OnOpenDecode(#[source] fidl::Error),
#[error("open failed with status: {0}")]
OpenError(#[source] zx_status::Status),
#[error("remote responded with success but provided no node info")]
MissingOnOpenInfo,
#[error("expected node to be a {expected:?}, but got a {actual:?}")]
UnexpectedNodeKind { expected: Kind, actual: Kind },
}
impl OpenError {
/// Returns true if the open failed because the node was not found.
pub fn is_not_found_error(&self) -> bool {
matches!(
self,
OpenError::OpenError(zx_status::Status::NOT_FOUND)
| OpenError::Namespace(zx_status::Status::NOT_FOUND)
)
}
}
/// An error encountered while cloning a node
#[derive(Debug, Clone, Error)]
#[allow(missing_docs)]
pub enum CloneError {
#[error("while making a fidl proxy: {0}")]
CreateProxy(#[source] fidl::Error),
#[error("while sending clone request: {0}")]
SendCloneRequest(#[source] fidl::Error),
}
/// An error encountered while closing a node
#[derive(Debug, Clone, Error)]
#[allow(missing_docs)]
pub enum CloseError {
#[error("while sending close request: {0}")]
SendCloseRequest(#[source] fidl::Error),
#[error("close failed with status: {0}")]
CloseError(#[source] zx_status::Status),
}
/// An error encountered while renaming a node
#[derive(Debug, Clone, Error)]
#[allow(missing_docs)]
pub enum RenameError {
#[error("while sending rename request")]
SendRenameRequest(#[source] fidl::Error),
#[error("while sending get_token request")]
SendGetTokenRequest(#[source] fidl::Error),
#[error("rename failed with status")]
RenameError(#[source] zx_status::Status),
#[error("while opening subdirectory")]
OpenError(#[from] OpenError),
#[error("get_token failed with status")]
GetTokenError(#[source] zx_status::Status),
#[error("no handle from get token")]
NoHandleError,
}
/// The type of a filesystem node
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Kind {
Service,
File,
Directory,
Symlink,
Unknown,
}
impl Kind {
pub(crate) fn kind_of(info: &fio::NodeInfoDeprecated) -> Kind {
match info {
fio::NodeInfoDeprecated::Service(_) => Kind::Service,
fio::NodeInfoDeprecated::File(_) => Kind::File,
fio::NodeInfoDeprecated::Directory(_) => Kind::Directory,
fio::NodeInfoDeprecated::Symlink(_) => Kind::Symlink,
}
}
fn expect_file(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
match info {
fio::NodeInfoDeprecated::File(fio::FileObject { .. }) => Ok(()),
other => Err(Kind::kind_of(&other)),
}
}
fn expect_directory(info: fio::NodeInfoDeprecated) -> Result<(), Kind> {
match info {
fio::NodeInfoDeprecated::Directory(fio::DirectoryObject) => Ok(()),
other => Err(Kind::kind_of(&other)),
}
}
pub(crate) fn kind_of2(representation: &fio::Representation) -> Kind {
match representation {
fio::Representation::Connector(_) => Kind::Service,
fio::Representation::Directory(_) => Kind::Directory,
fio::Representation::File(_) => Kind::File,
fio::Representation::Symlink(_) => Kind::Symlink,
_ => Kind::Unknown,
}
}
fn expect_file2(representation: &fio::Representation) -> Result<(), Kind> {
match representation {
fio::Representation::File(fio::FileInfo { .. }) => Ok(()),
other => Err(Kind::kind_of2(other)),
}
}
fn expect_directory2(representation: &fio::Representation) -> Result<(), Kind> {
match representation {
fio::Representation::Directory(_) => Ok(()),
other => Err(Kind::kind_of2(other)),
}
}
}
/// Opens the given `path` from the current namespace as a [`NodeProxy`].
///
/// The target is assumed to implement fuchsia.io.Node but this isn't verified. To connect to a
/// filesystem node which doesn't implement fuchsia.io.Node, use the functions in
/// [`fuchsia_component::client`] instead.
///
/// If the namespace path doesn't exist, or we fail to make the channel pair, this returns an
/// error. However, if incorrect flags are sent, or if the rest of the path sent to the filesystem
/// server doesn't exist, this will still return success. Instead, the returned NodeProxy channel
/// pair will be closed with an epitaph.
#[cfg(target_os = "fuchsia")]
pub fn open_in_namespace(path: &str, flags: fio::OpenFlags) -> Result<fio::NodeProxy, OpenError> {
let (node, request) = fidl::endpoints::create_proxy().map_err(OpenError::CreateProxy)?;
open_channel_in_namespace(path, flags, request)?;
Ok(node)
}
/// Asynchronously opens the given [`path`] in the current namespace, serving the connection over
/// [`request`]. Once the channel is connected, any calls made prior are serviced.
///
/// The target is assumed to implement fuchsia.io.Node but this isn't verified. To connect to a
/// filesystem node which doesn't implement fuchsia.io.Node, use the functions in
/// [`fuchsia_component::client`] instead.
///
/// If the namespace path doesn't exist, this returns an error. However, if incorrect flags are
/// sent, or if the rest of the path sent to the filesystem server doesn't exist, this will still
/// return success. Instead, the [`request`] channel will be closed with an epitaph.
#[cfg(target_os = "fuchsia")]
pub fn open_channel_in_namespace(
path: &str,
flags: fio::OpenFlags,
request: fidl::endpoints::ServerEnd<fio::NodeMarker>,
) -> Result<(), OpenError> {
let namespace = fdio::Namespace::installed().map_err(OpenError::Namespace)?;
namespace.open(path, flags, request.into_channel()).map_err(OpenError::Namespace)
}
/// Gracefully closes the node proxy from the remote end.
pub async fn close(node: fio::NodeProxy) -> Result<(), CloseError> {
let result = node.close().await.map_err(CloseError::SendCloseRequest)?;
result.map_err(|s| CloseError::CloseError(zx_status::Status::from_raw(s)))
}
/// Consume the first event from this NodeProxy's event stream, returning the proxy if it is
/// the expected type or an error otherwise.
pub(crate) async fn verify_node_describe_event(
node: fio::NodeProxy,
) -> Result<fio::NodeProxy, OpenError> {
match take_on_open_event(&node).await? {
fio::NodeEvent::OnOpen_ { s: status, info } => {
let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
info.ok_or(OpenError::MissingOnOpenInfo)?;
}
fio::NodeEvent::OnRepresentation { .. } => (),
}
Ok(node)
}
/// Consume the first event from this DirectoryProxy's event stream, returning the proxy if it is
/// the expected type or an error otherwise.
pub(crate) async fn verify_directory_describe_event(
node: fio::DirectoryProxy,
) -> Result<fio::DirectoryProxy, OpenError> {
match take_on_open_event(&node).await? {
fio::DirectoryEvent::OnOpen_ { s: status, info } => {
let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
let () = Kind::expect_directory(*info).map_err(|actual| {
OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
})?;
}
fio::DirectoryEvent::OnRepresentation { payload } => {
let () = Kind::expect_directory2(&payload).map_err(|actual| {
OpenError::UnexpectedNodeKind { expected: Kind::Directory, actual }
})?;
}
}
Ok(node)
}
/// Consume the first event from this FileProxy's event stream, returning the proxy if it is the
/// expected type or an error otherwise.
pub(crate) async fn verify_file_describe_event(
node: fio::FileProxy,
) -> Result<fio::FileProxy, OpenError> {
match take_on_open_event(&node).await? {
fio::FileEvent::OnOpen_ { s: status, info } => {
let () = zx_status::Status::ok(status).map_err(OpenError::OpenError)?;
let info = info.ok_or(OpenError::MissingOnOpenInfo)?;
let () = Kind::expect_file(*info)
.map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
}
fio::FileEvent::OnRepresentation { payload } => {
let () = Kind::expect_file2(&payload)
.map_err(|actual| OpenError::UnexpectedNodeKind { expected: Kind::File, actual })?;
}
}
Ok(node)
}
pub(crate) trait OnOpenEventProducer {
type Event;
type Stream: futures::Stream<Item = Result<Self::Event, fidl::Error>> + Unpin;
fn take_event_stream(&self) -> Self::Stream;
}
macro_rules! impl_on_open_event_producer {
($proxy:ty, $event:ty, $stream:ty) => {
impl OnOpenEventProducer for $proxy {
type Event = $event;
type Stream = $stream;
fn take_event_stream(&self) -> Self::Stream {
self.take_event_stream()
}
}
};
}
impl_on_open_event_producer!(fio::NodeProxy, fio::NodeEvent, fio::NodeEventStream);
impl_on_open_event_producer!(fio::FileProxy, fio::FileEvent, fio::FileEventStream);
impl_on_open_event_producer!(fio::DirectoryProxy, fio::DirectoryEvent, fio::DirectoryEventStream);
pub(crate) async fn take_on_open_event<T>(node: &T) -> Result<T::Event, OpenError>
where
T: OnOpenEventProducer,
{
node.take_event_stream()
.next()
.await
.ok_or(OpenError::OnOpenEventStreamClosed)?
.map_err(OpenError::OnOpenDecode)
}
#[cfg(test)]
mod tests {
use {super::*, crate::OpenFlags, assert_matches::assert_matches, fuchsia_async as fasync};
// open_in_namespace
#[fasync::run_singlethreaded(test)]
async fn open_in_namespace_opens_real_node() {
let file_node = open_in_namespace("/pkg/data/file", OpenFlags::RIGHT_READABLE).unwrap();
let protocol = file_node.query().await.unwrap();
assert_eq!(protocol, fio::FILE_PROTOCOL_NAME.as_bytes());
let dir_node = open_in_namespace("/pkg/data", OpenFlags::RIGHT_READABLE).unwrap();
let protocol = dir_node.query().await.unwrap();
assert_eq!(protocol, fio::DIRECTORY_PROTOCOL_NAME.as_bytes());
}
#[fasync::run_singlethreaded(test)]
async fn open_in_namespace_opens_fake_node_under_of_root_namespace_entry() {
let notfound = open_in_namespace("/pkg/fake", OpenFlags::RIGHT_READABLE).unwrap();
// The open error is not detected until the proxy is interacted with.
assert_matches!(close(notfound).await, Err(_));
}
#[fasync::run_singlethreaded(test)]
async fn open_in_namespace_rejects_fake_root_namespace_entry() {
assert_matches!(
open_in_namespace("/fake", OpenFlags::RIGHT_READABLE),
Err(OpenError::Namespace(zx_status::Status::NOT_FOUND))
);
}
}