| // Copyright 2022 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. |
| |
| //! Utilities for working with the `fuchsia.mem` FIDL library. |
| |
| use fidl_fuchsia_io as fio; |
| use fidl_fuchsia_mem as fmem; |
| use fuchsia_zircon_status as zxs; |
| use std::borrow::Cow; |
| |
| /// Open `path` from given `parent` directory, returning an [`fmem::Data`] of the contents. |
| /// |
| /// Prioritizes returning an [`fmem::Data::Buffer`] if it can be done by reusing a VMO handle |
| /// from the directory's server. |
| pub async fn open_file_data( |
| parent: &fio::DirectoryProxy, |
| path: &str, |
| ) -> Result<fmem::Data, FileError> { |
| let file = |
| fuchsia_fs::directory::open_file_no_describe(parent, path, fio::OpenFlags::RIGHT_READABLE)?; |
| match file |
| .get_backing_memory(fio::VmoFlags::READ) |
| .await |
| .map_err(|e| { |
| // Don't swallow the root cause of the error without a trace. It may |
| // be impossible to correlate resulting error to its root cause |
| // otherwise. |
| tracing::debug!("error for path={}: {}:", path, e); |
| FileError::GetBufferError(e) |
| })? |
| .map_err(zxs::Status::from_raw) |
| { |
| Ok(vmo) => { |
| let size = vmo.get_content_size().expect("failed to get VMO size"); |
| Ok(fmem::Data::Buffer(fmem::Buffer { vmo, size })) |
| } |
| Err(e) => { |
| let _: zxs::Status = e; |
| // we still didn't get a VMO handle, fallback to reads over the channel |
| let bytes = fuchsia_fs::file::read(&file).await?; |
| Ok(fmem::Data::Bytes(bytes)) |
| } |
| } |
| } |
| |
| /// Errors that can occur when operating on `DirectoryProxy`s and `FileProxy`s. |
| #[derive(Debug, thiserror::Error)] |
| pub enum FileError { |
| #[error("Failed to open a File.")] |
| OpenError(#[from] fuchsia_fs::node::OpenError), |
| |
| #[error("Couldn't read a file")] |
| ReadError(#[from] fuchsia_fs::file::ReadError), |
| |
| #[error("FIDL call to retrieve a file's buffer failed")] |
| GetBufferError(#[source] fidl::Error), |
| } |
| |
| /// Retrieve the bytes in `data`, returning a reference if it's a `Data::Bytes` and a copy of |
| /// the bytes read from the VMO if it's a `Data::Buffer`. |
| pub fn bytes_from_data<'d>(data: &'d fmem::Data) -> Result<Cow<'d, [u8]>, DataError> { |
| Ok(match data { |
| fmem::Data::Buffer(buf) => { |
| let size = buf.size as usize; |
| let mut raw_bytes = Vec::with_capacity(size); |
| raw_bytes.resize(size, 0); |
| buf.vmo.read(&mut raw_bytes, 0).map_err(DataError::VmoReadError)?; |
| Cow::Owned(raw_bytes) |
| } |
| fmem::Data::Bytes(b) => Cow::Borrowed(b), |
| fmem::DataUnknown!() => return Err(DataError::UnrecognizedDataVariant), |
| }) |
| } |
| |
| /// Errors that can occur when operating on `fuchsia.mem.Data` values. |
| #[derive(Debug, thiserror::Error, PartialEq, Eq)] |
| pub enum DataError { |
| #[error("Couldn't read from VMO")] |
| VmoReadError(#[source] zxs::Status), |
| |
| #[error("Encountered an unrecognized variant of fuchsia.mem.Data")] |
| UnrecognizedDataVariant, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use async_trait::async_trait; |
| use fidl::endpoints::{create_proxy, ServerEnd}; |
| use fuchsia_zircon_status::Status; |
| use futures::StreamExt; |
| use std::sync::Arc; |
| use vfs::{ |
| directory::{ |
| entry::{DirectoryEntry, EntryInfo, OpenRequest}, |
| entry_container::Directory, |
| }, |
| execution_scope::ExecutionScope, |
| file::vmo::read_only, |
| file::{FileLike, FileOptions}, |
| object_request::Representation, |
| pseudo_directory, ObjectRequestRef, |
| }; |
| |
| #[fuchsia::test] |
| async fn bytes_from_read_only() { |
| let fs = pseudo_directory! { |
| // `read_only` is a vmo file, returns the buffer in OnOpen |
| "foo" => read_only("hello, world!"), |
| }; |
| let directory = serve_vfs_dir(fs); |
| |
| let data = open_file_data(&directory, "foo").await.unwrap(); |
| match bytes_from_data(&data).unwrap() { |
| Cow::Owned(b) => assert_eq!(b, b"hello, world!"), |
| _ => panic!("must produce an owned value from reading contents of fmem::Data::Buffer"), |
| } |
| } |
| |
| /// Test that we get a VMO when the server supports `File/GetBackingMemory`. |
| #[fuchsia::test] |
| async fn bytes_from_vmo_from_get_buffer() { |
| let vmo_data = b"hello, world!"; |
| let fs = pseudo_directory! { |
| "foo" => read_only(vmo_data), |
| }; |
| let directory = serve_vfs_dir(fs); |
| |
| let data = open_file_data(&directory, "foo").await.unwrap(); |
| match bytes_from_data(&data).unwrap() { |
| Cow::Owned(b) => assert_eq!(b, vmo_data), |
| _ => panic!("must produce an owned value from reading contents of fmem::Data::Buffer"), |
| } |
| } |
| |
| /// Test that we correctly fall back to reading through FIDL calls in a channel if the server |
| /// doesn't support returning a VMO. |
| #[fuchsia::test] |
| async fn bytes_from_channel_fallback() { |
| // This test File does not handle `File/GetBackingMemory` request, but will return |
| // b"hello, world!" on File/Read`. |
| struct NonVMOTestFile; |
| |
| impl DirectoryEntry for NonVMOTestFile { |
| fn entry_info(&self) -> EntryInfo { |
| EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File) |
| } |
| |
| fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> { |
| request.open_file(self) |
| } |
| } |
| |
| #[async_trait] |
| impl vfs::node::Node for NonVMOTestFile { |
| async fn get_attrs(&self) -> Result<fio::NodeAttributes, Status> { |
| Err(Status::NOT_SUPPORTED) |
| } |
| |
| async fn get_attributes( |
| &self, |
| _requested_attributes: fio::NodeAttributesQuery, |
| ) -> Result<fio::NodeAttributes2, Status> { |
| Err(Status::NOT_SUPPORTED) |
| } |
| } |
| |
| impl FileLike for NonVMOTestFile { |
| fn open( |
| self: Arc<Self>, |
| scope: ExecutionScope, |
| _options: FileOptions, |
| object_request: ObjectRequestRef<'_>, |
| ) -> Result<(), Status> { |
| struct Connection; |
| impl Representation for Connection { |
| type Protocol = fio::FileMarker; |
| |
| async fn get_representation( |
| &self, |
| _requested_attributes: fio::NodeAttributesQuery, |
| ) -> Result<fio::Representation, Status> { |
| unreachable!() |
| } |
| |
| async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> { |
| unreachable!() |
| } |
| } |
| let connection = Connection; |
| let object_request = object_request.take(); |
| scope.spawn(async move { |
| if let Ok(mut file_requests) = |
| object_request.into_request_stream(&connection).await |
| { |
| let mut have_sent_bytes = false; |
| while let Some(Ok(request)) = file_requests.next().await { |
| match request { |
| fio::FileRequest::GetBackingMemory { flags: _, responder } => { |
| responder.send(Err(Status::NOT_SUPPORTED.into_raw())).unwrap() |
| } |
| fio::FileRequest::Read { count: _, responder } => { |
| let to_send: &[u8] = if !have_sent_bytes { |
| have_sent_bytes = true; |
| b"hello, world!" |
| } else { |
| &[] |
| }; |
| responder.send(Ok(to_send)).unwrap(); |
| } |
| unexpected => unimplemented!("{:#?}", unexpected), |
| } |
| } |
| } |
| }); |
| Ok(()) |
| } |
| } |
| |
| let fs = pseudo_directory! { |
| "foo" => Arc::new(NonVMOTestFile), |
| }; |
| let directory = serve_vfs_dir(fs); |
| |
| let data = open_file_data(&directory, "foo").await.unwrap(); |
| let data = bytes_from_data(&data).unwrap(); |
| assert_eq!( |
| data, |
| Cow::Borrowed(b"hello, world!"), |
| "must produce a borrowed value from fmem::Data::Bytes" |
| ); |
| } |
| |
| fn serve_vfs_dir(root: Arc<impl Directory>) -> fio::DirectoryProxy { |
| let fs_scope = ExecutionScope::new(); |
| let (client, server) = create_proxy::<fio::DirectoryMarker>().unwrap(); |
| root.open( |
| fs_scope.clone(), |
| fio::OpenFlags::RIGHT_READABLE, |
| vfs::path::Path::dot(), |
| ServerEnd::new(server.into_channel()), |
| ); |
| client |
| } |
| } |