blob: b6612aa0db4b54f915606a38a35ac30af167f683 [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 files.
use {
crate::node::{self, CloseError, OpenError},
fidl_fuchsia_io::{FileMarker, FileProxy, MAX_BUF},
fuchsia_zircon::Status,
thiserror::Error,
};
/// 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(Status),
#[error("file was not a utf-8 encoded string: {}", _0)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
}
/// 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(Status),
#[error("file endpoint reported more bytes written than were provided")]
Overwrite,
}
/// Opens the given `path` from the current namespace as a [`FileProxy`]. The target is not
/// verified to be any particular type and may not implement the fuchsia.io.File protocol.
pub fn open_in_namespace(path: &str, flags: u32) -> Result<FileProxy, OpenError> {
let (dir, server_end) =
fidl::endpoints::create_proxy::<FileMarker>().map_err(OpenError::CreateProxy)?;
node::connect_in_namespace(path, flags, server_end.into_channel())
.map_err(OpenError::Namespace)?;
Ok(dir)
}
/// Gracefully closes the file proxy from the remote end.
pub async fn close(file: FileProxy) -> Result<(), CloseError> {
let status = file.close().await.map_err(CloseError::SendCloseRequest)?;
Status::ok(status).map_err(CloseError::CloseError)
}
/// 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.
pub async fn write_in_namespace<D>(path: &str, data: D) -> Result<(), WriteError>
where
D: AsRef<[u8]>,
{
let flags = fidl_fuchsia_io::OPEN_RIGHT_WRITABLE
| fidl_fuchsia_io::OPEN_FLAG_CREATE
| fidl_fuchsia_io::OPEN_FLAG_TRUNCATE;
let file = open_in_namespace(path, flags)?;
write(&file, data).await?;
let _ = close(file).await;
Ok(())
}
/// Writes the given data into the given file.
pub async fn write<D>(file: &FileProxy, data: D) -> Result<(), WriteError>
where
D: AsRef<[u8]>,
{
let mut data = data.as_ref();
while data.len() > 0 {
let (status, bytes_written) =
file.write(&data[..std::cmp::min(MAX_BUF as usize, data.len())]).await?;
Status::ok(status).map_err(WriteError::WriteError)?;
if bytes_written > data.len() as u64 {
return Err(WriteError::Overwrite);
}
data = &data[bytes_written as usize..];
}
Ok(())
}
/// Reads all data from the given file's current offset to the end of the file.
pub async fn read(file: &FileProxy) -> Result<Vec<u8>, ReadError> {
let mut out = Vec::new();
loop {
let (status, mut bytes) = file.read(MAX_BUF).await?;
Status::ok(status).map_err(ReadError::ReadError)?;
if bytes.is_empty() {
break;
}
out.append(&mut bytes);
}
Ok(out)
}
/// 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: &FileProxy) -> Result<String, ReadError> {
let bytes = read(file).await?;
let string = String::from_utf8(bytes)?;
Ok(string)
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{directory, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE},
fuchsia_async as fasync,
matches::assert_matches,
std::path::Path,
tempfile::TempDir,
};
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", OPEN_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", OPEN_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", OPEN_RIGHT_READABLE),
Err(OpenError::Namespace(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);
}
// 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(),
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
)
.unwrap();
// Write contents.
let flags = fidl_fuchsia_io::OPEN_RIGHT_WRITABLE | fidl_fuchsia_io::OPEN_FLAG_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(),
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
)
.unwrap();
// Write contents.
let flags = fidl_fuchsia_io::OPEN_RIGHT_WRITABLE | fidl_fuchsia_io::OPEN_FLAG_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(),
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
)
.unwrap();
// Create and write to the file.
let flags = fidl_fuchsia_io::OPEN_RIGHT_WRITABLE | fidl_fuchsia_io::OPEN_FLAG_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", OPEN_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", OPEN_RIGHT_READABLE).unwrap();
// Advance past the first byte.
file.read(1).await.unwrap();
// Verify the rest of the file is read.
let contents = read(&file).await.unwrap();
assert_eq!(&contents[..], "ello World!\n".as_bytes());
}
// read_to_string
#[fasync::run_singlethreaded(test)]
async fn read_to_string_reads_data_file() {
let file = open_in_namespace("/pkg/data/file", OPEN_RIGHT_READABLE).unwrap();
assert_eq!(read_to_string(&file).await.unwrap(), DATA_FILE_CONTENTS);
}
}