| // 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 files. |
| |
| use { |
| crate::node::{CloseError, OpenError}, |
| fidl::{persist, unpersist, Persistable}, |
| fidl_fuchsia_io as fio, fuchsia_zircon_status as zx_status, |
| thiserror::Error, |
| }; |
| |
| mod async_reader; |
| pub use async_reader::AsyncReader; |
| |
| mod async_read_at; |
| pub use async_read_at::{Adapter, AsyncFile, AsyncGetSize, AsyncGetSizeExt, AsyncReadAt}; |
| mod async_read_at_ext; |
| pub use async_read_at_ext::AsyncReadAtExt; |
| mod buffered_async_read_at; |
| pub use buffered_async_read_at::BufferedAsyncReadAt; |
| |
| #[cfg(target_os = "fuchsia")] |
| use { |
| crate::node::{take_on_open_event, Kind}, |
| fuchsia_zircon::{self as zx}, |
| }; |
| |
| /// An error encountered while reading a file |
| #[derive(Debug, Error)] |
| #[allow(missing_docs)] |
| pub enum ReadError { |
| #[error("while opening the file: {0}")] |
| Open(#[from] OpenError), |
| |
| #[error("read call failed: {0}")] |
| Fidl(#[from] fidl::Error), |
| |
| #[error("read failed with status: {0}")] |
| ReadError(#[source] zx_status::Status), |
| |
| #[error("file was not a utf-8 encoded string: {0}")] |
| InvalidUtf8(#[from] std::string::FromUtf8Error), |
| } |
| |
| impl ReadError { |
| /// Returns true if the read failed because the file was no found. |
| pub fn is_not_found_error(&self) -> bool { |
| matches!(self, ReadError::Open(e) if e.is_not_found_error()) |
| } |
| } |
| |
| /// An error encountered while reading a named file |
| #[derive(Debug, Error)] |
| #[error("error reading '{path}': {source}")] |
| pub struct ReadNamedError { |
| path: String, |
| |
| #[source] |
| source: ReadError, |
| } |
| |
| impl ReadNamedError { |
| /// Returns the path associated with this error. |
| pub fn path(&self) -> &str { |
| &self.path |
| } |
| |
| /// Unwraps the inner read error, discarding the associated path. |
| pub fn into_inner(self) -> ReadError { |
| self.source |
| } |
| |
| /// Returns true if the read failed because the file was no found. |
| pub fn is_not_found_error(&self) -> bool { |
| self.source.is_not_found_error() |
| } |
| } |
| |
| /// An error encountered while writing a file |
| #[derive(Debug, Error)] |
| #[allow(missing_docs)] |
| pub enum WriteError { |
| #[error("while creating the file: {0}")] |
| Create(#[from] OpenError), |
| |
| #[error("write call failed: {0}")] |
| Fidl(#[from] fidl::Error), |
| |
| #[error("write failed with status: {0}")] |
| WriteError(#[source] zx_status::Status), |
| |
| #[error("file endpoint reported more bytes written than were provided")] |
| Overwrite, |
| } |
| |
| /// An error encountered while writing a named file |
| #[derive(Debug, Error)] |
| #[error("error writing '{path}': {source}")] |
| pub struct WriteNamedError { |
| path: String, |
| |
| #[source] |
| source: WriteError, |
| } |
| |
| impl WriteNamedError { |
| /// Returns the path associated with this error. |
| pub fn path(&self) -> &str { |
| &self.path |
| } |
| |
| /// Unwraps the inner write error, discarding the associated path. |
| pub fn into_inner(self) -> WriteError { |
| self.source |
| } |
| } |
| |
| /// Opens the given `path` from the current namespace as a [`FileProxy`]. |
| /// |
| /// The target is assumed to implement fuchsia.io.File but this isn't verified. To connect to a |
| /// filesystem node which doesn't implement fuchsia.io.File, 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 FileProxy channel |
| /// pair will be closed with an epitaph. |
| #[cfg(target_os = "fuchsia")] |
| pub fn open_in_namespace(path: &str, flags: fio::OpenFlags) -> Result<fio::FileProxy, 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.File but this isn't verified. To connect to a |
| /// filesystem node which doesn't implement fuchsia.io.File, 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::FileMarker>, |
| ) -> 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 file proxy from the remote end. |
| pub async fn close(file: fio::FileProxy) -> Result<(), CloseError> { |
| let result = file.close().await.map_err(CloseError::SendCloseRequest)?; |
| result.map_err(|s| CloseError::CloseError(zx_status::Status::from_raw(s))) |
| } |
| |
| /// Write the given data into a file at `path` in the current namespace. The path must be an |
| /// absolute path. |
| /// * If the file already exists, replaces existing contents. |
| /// * If the file does not exist, creates the file. |
| #[cfg(target_os = "fuchsia")] |
| pub async fn write_in_namespace<D>(path: &str, data: D) -> Result<(), WriteNamedError> |
| where |
| D: AsRef<[u8]>, |
| { |
| async { |
| let flags = |
| fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE | fio::OpenFlags::TRUNCATE; |
| let file = open_in_namespace(path, flags)?; |
| |
| write(&file, data).await?; |
| |
| let _ = close(file).await; |
| Ok(()) |
| } |
| .await |
| .map_err(|source| WriteNamedError { path: path.to_owned(), source }) |
| } |
| |
| /// Writes the given data into the given file. |
| pub async fn write<D>(file: &fio::FileProxy, data: D) -> Result<(), WriteError> |
| where |
| D: AsRef<[u8]>, |
| { |
| let mut data = data.as_ref(); |
| |
| while !data.is_empty() { |
| let bytes_written = file |
| .write(&data[..std::cmp::min(fio::MAX_BUF as usize, data.len())]) |
| .await? |
| .map_err(|s| WriteError::WriteError(zx_status::Status::from_raw(s)))?; |
| |
| if bytes_written > data.len() as u64 { |
| return Err(WriteError::Overwrite); |
| } |
| |
| data = &data[bytes_written as usize..]; |
| } |
| Ok(()) |
| } |
| |
| /// Write the given FIDL message in a binary form into a file open for writing. |
| pub async fn write_fidl<T: Persistable>( |
| file: &fio::FileProxy, |
| data: &mut T, |
| ) -> Result<(), WriteError> { |
| write(file, persist(data)?).await?; |
| Ok(()) |
| } |
| |
| /// Write the given FIDL encoded message into a file at `path`. The path must be an absolute path. |
| /// * If the file already exists, replaces existing contents. |
| /// * If the file does not exist, creates the file. |
| #[cfg(target_os = "fuchsia")] |
| pub async fn write_fidl_in_namespace<T: Persistable>( |
| path: &str, |
| data: &mut T, |
| ) -> Result<(), WriteNamedError> { |
| let data = persist(data) |
| .map_err(|source| WriteNamedError { path: path.to_owned(), source: source.into() })?; |
| write_in_namespace(path, data).await?; |
| Ok(()) |
| } |
| |
| /// Reads all data from the given file's current offset to the end of the file. |
| pub async fn read(file: &fio::FileProxy) -> Result<Vec<u8>, ReadError> { |
| let mut out = Vec::new(); |
| |
| loop { |
| let mut bytes = file |
| .read(fio::MAX_BUF) |
| .await? |
| .map_err(|s| ReadError::ReadError(zx_status::Status::from_raw(s)))?; |
| if bytes.is_empty() { |
| break; |
| } |
| out.append(&mut bytes); |
| } |
| Ok(out) |
| } |
| |
| /// Attempts to read a number of bytes from the given file's current offset. |
| /// This function may return less data than expected. |
| pub async fn read_num_bytes(file: &fio::FileProxy, num_bytes: u64) -> Result<Vec<u8>, ReadError> { |
| let mut data = vec![]; |
| |
| // Read in chunks of |MAX_BUF| bytes. |
| // This is the maximum buffer size supported over FIDL. |
| let mut bytes_left = num_bytes; |
| while bytes_left > 0 { |
| let bytes_to_read = std::cmp::min(bytes_left, fio::MAX_BUF); |
| let mut bytes = file |
| .read(bytes_to_read) |
| .await? |
| .map_err(|s| ReadError::ReadError(zx_status::Status::from_raw(s)))?; |
| |
| if bytes.is_empty() { |
| break; |
| } |
| |
| bytes_left -= bytes.len() as u64; |
| data.append(&mut bytes); |
| } |
| |
| // Remove excess data read in, if any. |
| let num_bytes = num_bytes as usize; |
| if data.len() > num_bytes { |
| data.drain(num_bytes..data.len()); |
| } |
| |
| Ok(data) |
| } |
| |
| /// Reads all data from the file at `path` in the current namespace. The path must be an absolute |
| /// path. |
| #[cfg(target_os = "fuchsia")] |
| pub async fn read_in_namespace(path: &str) -> Result<Vec<u8>, ReadNamedError> { |
| async { |
| let file = open_in_namespace( |
| path, |
| fio::OpenFlags::DESCRIBE |
| | fio::OpenFlags::RIGHT_READABLE |
| | fio::OpenFlags::NOT_DIRECTORY, |
| )?; |
| read_file_with_on_open_event(file).await |
| } |
| .await |
| .map_err(|source| ReadNamedError { path: path.to_owned(), source }) |
| } |
| |
| /// Reads a utf-8 encoded string from the given file's current offset to the end of the file. |
| pub async fn read_to_string(file: &fio::FileProxy) -> Result<String, ReadError> { |
| let bytes = read(file).await?; |
| let string = String::from_utf8(bytes)?; |
| Ok(string) |
| } |
| |
| /// Reads a utf-8 encoded string from the file at `path` in the current namespace. The path must be |
| /// an absolute path. |
| #[cfg(target_os = "fuchsia")] |
| pub async fn read_in_namespace_to_string(path: &str) -> Result<String, ReadNamedError> { |
| let bytes = read_in_namespace(path).await?; |
| let string = String::from_utf8(bytes) |
| .map_err(|source| ReadNamedError { path: path.to_owned(), source: source.into() })?; |
| Ok(string) |
| } |
| |
| /// Read the given FIDL message from binary form from a file open for reading. |
| /// FIDL structure should be provided at a read time. |
| /// Incompatible data is populated as per FIDL ABI compatibility guide: |
| /// https://fuchsia.dev/fuchsia-src/development/languages/fidl/guides/abi-compat |
| pub async fn read_fidl<T: Persistable>(file: &fio::FileProxy) -> Result<T, ReadError> { |
| let bytes = read(file).await?; |
| Ok(unpersist(&bytes)?) |
| } |
| |
| /// Read the given FIDL message from binary file at `path` in the current namespace. The path |
| /// must be an absolute path. |
| /// FIDL structure should be provided at a read time. |
| /// Incompatible data is populated as per FIDL ABI compatibility guide: |
| /// https://fuchsia.dev/fuchsia-src/development/languages/fidl/guides/abi-compat |
| #[cfg(target_os = "fuchsia")] |
| pub async fn read_in_namespace_to_fidl<T: Persistable>(path: &str) -> Result<T, ReadNamedError> { |
| let bytes = read_in_namespace(path).await?; |
| unpersist(&bytes) |
| .map_err(|source| ReadNamedError { path: path.to_owned(), source: source.into() }) |
| } |
| |
| /// Extracts the stream from an OnOpen or OnRepresentation FileEvent. |
| #[cfg(target_os = "fuchsia")] |
| fn extract_stream_from_on_open_event( |
| event: fio::FileEvent, |
| ) -> Result<Option<zx::Stream>, OpenError> { |
| match event { |
| fio::FileEvent::OnOpen_ { s: status, info } => { |
| zx::Status::ok(status).map_err(OpenError::OpenError)?; |
| let node_info = info.ok_or(OpenError::MissingOnOpenInfo)?; |
| match *node_info { |
| fio::NodeInfoDeprecated::File(file_info) => Ok(file_info.stream), |
| node_info @ _ => Err(OpenError::UnexpectedNodeKind { |
| expected: Kind::File, |
| actual: Kind::kind_of(&node_info), |
| }), |
| } |
| } |
| fio::FileEvent::OnRepresentation { payload } => match payload { |
| fio::Representation::File(file_info) => Ok(file_info.stream), |
| representation @ _ => Err(OpenError::UnexpectedNodeKind { |
| expected: Kind::File, |
| actual: Kind::kind_of2(&representation), |
| }), |
| }, |
| } |
| } |
| |
| /// Reads the contents of a stream into a Vec. |
| #[cfg(target_os = "fuchsia")] |
| fn read_contents_of_stream(stream: zx::Stream) -> Result<Vec<u8>, ReadError> { |
| // TODO(https://fxbug.dev/324239375): Get the file size from the OnRepresentation event. |
| let file_size = stream.seek(std::io::SeekFrom::End(0)).map_err(ReadError::ReadError)? as usize; |
| let mut data = Vec::with_capacity(file_size); |
| let mut remaining = file_size; |
| while remaining > 0 { |
| // read_at is used instead of read because the seek offset was moved to the end of the file |
| // to determine the file size. Moving the seek offset back to the start of the file would |
| // require another syscall. |
| let actual = stream |
| .read_at_uninit( |
| zx::StreamReadOptions::empty(), |
| data.len() as u64, |
| &mut data.spare_capacity_mut()[0..remaining], |
| ) |
| .map_err(ReadError::ReadError)?; |
| // A read of 0 bytes indicates the end of the file was reached. The file may have changed |
| // size since the seek. |
| if actual == 0 { |
| break; |
| } |
| // SAFETY: read_at_uninit returns the number of bytes that were read and initialized. |
| unsafe { data.set_len(data.len() + actual) }; |
| remaining -= actual; |
| } |
| Ok(data) |
| } |
| |
| /// Reads the contents of `file` into a Vec. `file` must have been opened with either `DESCRIBE` or |
| /// `GET_REPRESENTATION` and the event must not have been read yet. |
| #[cfg(target_os = "fuchsia")] |
| pub(crate) async fn read_file_with_on_open_event( |
| file: fio::FileProxy, |
| ) -> Result<Vec<u8>, ReadError> { |
| let event = take_on_open_event(&file).await.map_err(ReadError::Open)?; |
| let stream = extract_stream_from_on_open_event(event).map_err(ReadError::Open)?; |
| |
| if let Some(stream) = stream { |
| read_contents_of_stream(stream) |
| } else { |
| // Fall back to FIDL reads if the file doesn't support streams. |
| read(&file).await |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{directory, OpenFlags}, |
| assert_matches::assert_matches, |
| fidl_fidl_test_schema::{DataTable1, DataTable2}, |
| fuchsia_async as fasync, |
| fuchsia_zircon::{self as zx, HandleBased as _}, |
| std::{path::Path, sync::Arc}, |
| tempfile::TempDir, |
| vfs::{ |
| execution_scope::ExecutionScope, |
| file::vmo::{read_only, VmoFile}, |
| ToObjectRequest, |
| }, |
| }; |
| |
| const DATA_FILE_CONTENTS: &str = "Hello World!\n"; |
| |
| // open_in_namespace |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn open_in_namespace_opens_real_file() { |
| let exists = open_in_namespace("/pkg/data/file", OpenFlags::RIGHT_READABLE).unwrap(); |
| assert_matches!(close(exists).await, Ok(())); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn open_in_namespace_opens_fake_file_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)) |
| ); |
| } |
| |
| // write_in_namespace |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_in_namespace_creates_file() { |
| let tempdir = TempDir::new().unwrap(); |
| let path = tempdir.path().join(Path::new("new-file")).to_str().unwrap().to_owned(); |
| |
| // Write contents. |
| let data = b"\x80"; // Non UTF-8 data: a continuation byte as the first byte. |
| write_in_namespace(&path, data).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read(&path).unwrap(); |
| assert_eq!(&contents, &data); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_in_namespace_overwrites_existing_file() { |
| let tempdir = TempDir::new().unwrap(); |
| let path = tempdir.path().join(Path::new("existing-file")).to_str().unwrap().to_owned(); |
| |
| // Write contents. |
| let original_data = b"\x80\x81"; // Non UTF-8 data: a continuation byte as the first byte. |
| write_in_namespace(&path, original_data).await.unwrap(); |
| |
| // Over-write contents. |
| let new_data = b"\x82"; // Non UTF-8 data: a continuation byte as the first byte. |
| write_in_namespace(&path, new_data).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read(&path).unwrap(); |
| assert_eq!(&contents, &new_data); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_in_namespace_fails_on_invalid_namespace_entry() { |
| assert_matches!( |
| write_in_namespace("/fake", b"").await, |
| Err(WriteNamedError { path, source: WriteError::Create(_) }) if path == "/fake" |
| ); |
| let err = write_in_namespace("/fake", b"").await.unwrap_err(); |
| assert_eq!(err.path(), "/fake"); |
| assert_matches!(err.into_inner(), WriteError::Create(_)); |
| } |
| |
| // write |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_writes_to_file() { |
| let tempdir = TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| // Write contents. |
| let flags = fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE; |
| let file = directory::open_file(&dir, "file", flags).await.unwrap(); |
| let data = b"\x80"; // Non UTF-8 data: a continuation byte as the first byte. |
| write(&file, data).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read(tempdir.path().join(Path::new("file"))).unwrap(); |
| assert_eq!(&contents, &data); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_writes_to_file_in_chunks_if_needed() { |
| let tempdir = TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| // Write contents. |
| let flags = fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE; |
| let file = directory::open_file(&dir, "file", flags).await.unwrap(); |
| let data = "abc".repeat(10000); |
| write(&file, &data).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read_to_string(tempdir.path().join(Path::new("file"))).unwrap(); |
| assert_eq!(&contents, &data); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_appends_to_file() { |
| let tempdir = TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| // Create and write to the file. |
| let flags = fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE; |
| let file = directory::open_file(&dir, "file", flags).await.unwrap(); |
| write(&file, "Hello ").await.unwrap(); |
| write(&file, "World!\n").await.unwrap(); |
| close(file).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read(tempdir.path().join(Path::new("file"))).unwrap(); |
| assert_eq!(&contents[..], DATA_FILE_CONTENTS.as_bytes()); |
| } |
| |
| // read |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_reads_to_end_of_file() { |
| let file = open_in_namespace("/pkg/data/file", OpenFlags::RIGHT_READABLE).unwrap(); |
| |
| let contents = read(&file).await.unwrap(); |
| assert_eq!(&contents[..], DATA_FILE_CONTENTS.as_bytes()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_reads_from_current_position() { |
| let file = open_in_namespace("/pkg/data/file", OpenFlags::RIGHT_READABLE).unwrap(); |
| |
| // Advance past the first byte. |
| let _: Vec<u8> = file.read(1).await.unwrap().unwrap(); |
| |
| // Verify the rest of the file is read. |
| let contents = read(&file).await.unwrap(); |
| assert_eq!(&contents[..], "ello World!\n".as_bytes()); |
| } |
| |
| // read_in_namespace |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_in_namespace_reads_contents() { |
| let contents = read_in_namespace("/pkg/data/file").await.unwrap(); |
| assert_eq!(&contents[..], DATA_FILE_CONTENTS.as_bytes()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_in_namespace_fails_on_invalid_namespace_entry() { |
| assert_matches!( |
| read_in_namespace("/fake").await, |
| Err(ReadNamedError { path, source: ReadError::Open(_) }) if path == "/fake" |
| ); |
| let err = read_in_namespace("/fake").await.unwrap_err(); |
| assert_eq!(err.path(), "/fake"); |
| assert_matches!(err.into_inner(), ReadError::Open(_)); |
| } |
| |
| // read_to_string |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_to_string_reads_data_file() { |
| let file = open_in_namespace("/pkg/data/file", OpenFlags::RIGHT_READABLE).unwrap(); |
| assert_eq!(read_to_string(&file).await.unwrap(), DATA_FILE_CONTENTS); |
| } |
| |
| // read_in_namespace_to_string |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_in_namespace_to_string_reads_data_file() { |
| assert_eq!( |
| read_in_namespace_to_string("/pkg/data/file").await.unwrap(), |
| DATA_FILE_CONTENTS |
| ); |
| } |
| |
| // write_fidl |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn write_fidl_writes_to_file() { |
| let tempdir = TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| // Write contents. |
| let flags = fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE; |
| let file = directory::open_file(&dir, "file", flags).await.unwrap(); |
| |
| let mut data = DataTable1 { |
| num: Some(42), |
| string: Some(DATA_FILE_CONTENTS.to_string()), |
| ..Default::default() |
| }; |
| |
| // Binary encoded FIDL message, with header and padding. |
| let fidl_bytes = persist(&data).unwrap(); |
| |
| write_fidl(&file, &mut data).await.unwrap(); |
| |
| // Verify contents. |
| let contents = std::fs::read(tempdir.path().join(Path::new("file"))).unwrap(); |
| assert_eq!(&contents, &fidl_bytes); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_fidl_reads_from_file() { |
| let file = open_in_namespace("/pkg/data/fidl_file", OpenFlags::RIGHT_READABLE).unwrap(); |
| |
| let contents = read_fidl::<DataTable2>(&file).await.unwrap(); |
| |
| let data = DataTable2 { |
| num: Some(42), |
| string: Some(DATA_FILE_CONTENTS.to_string()), |
| new_field: None, |
| ..Default::default() |
| }; |
| assert_eq!(&contents, &data); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_open_event_with_stream() { |
| let vmo = zx::Vmo::create(0).unwrap(); |
| let stream = zx::Stream::create(zx::StreamOptions::empty(), &vmo, 0).unwrap(); |
| let event = fio::FileEvent::OnOpen_ { |
| s: 0, |
| info: Some(Box::new(fio::NodeInfoDeprecated::File(fio::FileObject { |
| stream: Some(stream), |
| event: None, |
| }))), |
| }; |
| let stream = extract_stream_from_on_open_event(event) |
| .expect("Not a file") |
| .expect("Stream not present"); |
| assert!(!stream.is_invalid_handle()); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_open_event_without_stream() { |
| let event = fio::FileEvent::OnOpen_ { |
| s: 0, |
| info: Some(Box::new(fio::NodeInfoDeprecated::File(fio::FileObject { |
| stream: None, |
| event: None, |
| }))), |
| }; |
| let stream = extract_stream_from_on_open_event(event).expect("Not a file"); |
| assert!(stream.is_none()); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_open_event_with_open_error() { |
| let event = fio::FileEvent::OnOpen_ { s: zx::Status::NOT_FOUND.into_raw(), info: None }; |
| let result = extract_stream_from_on_open_event(event); |
| assert_matches!(result, Err(OpenError::OpenError(zx::Status::NOT_FOUND))); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_open_event_not_a_file() { |
| let event = fio::FileEvent::OnOpen_ { |
| s: 0, |
| info: Some(Box::new(fio::NodeInfoDeprecated::Service(fio::Service))), |
| }; |
| let result = extract_stream_from_on_open_event(event); |
| assert_matches!( |
| result, |
| Err(OpenError::UnexpectedNodeKind { expected: Kind::File, actual: Kind::Service }) |
| ); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_representation_event_with_stream() { |
| let vmo = zx::Vmo::create(0).unwrap(); |
| let stream = zx::Stream::create(zx::StreamOptions::empty(), &vmo, 0).unwrap(); |
| let event = fio::FileEvent::OnRepresentation { |
| payload: fio::Representation::File(fio::FileInfo { |
| stream: Some(stream), |
| ..Default::default() |
| }), |
| }; |
| let stream = extract_stream_from_on_open_event(event) |
| .expect("Not a file") |
| .expect("Stream not present"); |
| assert!(!stream.is_invalid_handle()); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_representation_event_without_stream() { |
| let event = fio::FileEvent::OnRepresentation { |
| payload: fio::Representation::File(fio::FileInfo::default()), |
| }; |
| let stream = extract_stream_from_on_open_event(event).expect("Not a file"); |
| assert!(stream.is_none()); |
| } |
| |
| #[test] |
| fn extract_stream_from_on_representation_event_not_a_file() { |
| let event = fio::FileEvent::OnRepresentation { |
| payload: fio::Representation::Connector(fio::ConnectorInfo::default()), |
| }; |
| let result = extract_stream_from_on_open_event(event); |
| assert_matches!( |
| result, |
| Err(OpenError::UnexpectedNodeKind { expected: Kind::File, actual: Kind::Service }) |
| ); |
| } |
| |
| #[test] |
| fn read_contents_of_stream_with_contents() { |
| let data = b"file-contents".repeat(1000); |
| let vmo = zx::Vmo::create(data.len() as u64).unwrap(); |
| vmo.write(&data, 0).unwrap(); |
| let stream = zx::Stream::create(zx::StreamOptions::MODE_READ, &vmo, 0).unwrap(); |
| let contents = read_contents_of_stream(stream).unwrap(); |
| assert_eq!(contents, data); |
| } |
| |
| #[test] |
| fn read_contents_of_stream_with_empty_stream() { |
| let vmo = zx::Vmo::create(0).unwrap(); |
| let stream = zx::Stream::create(zx::StreamOptions::MODE_READ, &vmo, 0).unwrap(); |
| let contents = read_contents_of_stream(stream).unwrap(); |
| assert!(contents.is_empty()); |
| } |
| |
| fn serve_file(file: Arc<VmoFile>, flags: fio::OpenFlags) -> fio::FileProxy { |
| let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>().unwrap(); |
| flags.to_object_request(server_end).handle(|object_request| { |
| vfs::file::serve(file, ExecutionScope::new(), &flags, object_request) |
| }); |
| proxy |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_file_with_on_open_event_with_stream() { |
| let data = b"file-contents".repeat(1000); |
| let vmo_file = read_only(&data); |
| let flags = fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::DESCRIBE; |
| |
| { |
| // Ensure that the file supports streams. |
| let file = serve_file(vmo_file.clone(), flags); |
| let event = take_on_open_event(&file).await.unwrap(); |
| extract_stream_from_on_open_event(event).unwrap().expect("Stream not present"); |
| } |
| |
| let file = serve_file(vmo_file.clone(), flags); |
| let contents = read_file_with_on_open_event(file).await.unwrap(); |
| assert_eq!(contents, data); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn read_missing_file_in_namespace() { |
| assert_matches!( |
| read_in_namespace("/pkg/data/missing").await, |
| Err(e) if e.is_not_found_error() |
| ); |
| } |
| } |