blob: 70ebffaf909bb0eb2fd07ed516c770e2bb052d95 [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.
//! Simple Rust utilities for file I/O that are usable on the host (via overnet
//! FIDL usage) as well as on a Fuchsia target.
//!
//! For now all utilities in here are implemented as read-only.
use {
anyhow::{anyhow, Context, Result},
async_trait::async_trait,
byteorder::NetworkEndian,
fidl_fuchsia_io as fio,
fuchsia_zircon_status::Status,
futures::Future,
packet::BufferView,
std::convert::{TryFrom, TryInto},
std::fmt::Debug,
zerocopy::ByteSlice,
};
/// Applies a predicate to all children. If all children are known to be of
/// a certain type in advance, this function can be run much simpler. For
/// example, this trait is implemented for `MapChildren<DirectoryProxy, _, _>`,
/// which will automatically open each child in the directory as a directory.
#[async_trait]
pub trait MapChildren<T, E, F>
where
F: Future<Output = Result<E>>,
{
/// Attempts to apply the predicate to all children, using the flags for
/// opening each child.
async fn map_children(self, flags: u32, f: fn(T) -> F) -> Result<Vec<E>>;
}
#[async_trait]
impl<E, F> MapChildren<fio::DirectoryProxy, E, F> for fio::DirectoryProxy
where
F: Future<Output = Result<E>> + 'static + Send,
E: 'static + Send,
{
async fn map_children(self, flags: u32, func: fn(fio::DirectoryProxy) -> F) -> Result<Vec<E>> {
// TODO(awdavies): Run this in parallel with some kind of barrier to
// prevent overspawning channels.
let mut res = Vec::new();
for child in self.dirents().await? {
let child_root = child.to_dir_proxy(&self, flags)?;
res.push(func(child_root).await?);
}
Ok(res)
}
}
#[async_trait]
trait DirectoryProxyExtPrivate {
async fn dirent_bytes(&self) -> Result<Vec<u8>>;
}
#[async_trait]
impl DirectoryProxyExtPrivate for fio::DirectoryProxy {
async fn dirent_bytes(&self) -> Result<Vec<u8>> {
let (status, buf) = self.read_dirents(fio::MAX_BUF).await.context("read_dirents")?;
Status::ok(status).context("dirents result")?;
Ok(buf)
}
}
#[async_trait]
pub trait DirectoryProxyExt {
/// Reads all raw dirents, returning a vector of dirents.
/// Each dirent can be potentially converted into either a file or
/// directory proxy.
async fn dirents(&self) -> Result<Vec<Dirent>>;
/// Attempts to find a dirent in this directory, returning `None` if it
/// cannot be found.
async fn dirent(&self, name: &str) -> Result<Option<Dirent>>;
/// Attempts to open a directory at the given path.
fn open_dir(&self, path: &str, flags: u32) -> Result<fio::DirectoryProxy>;
/// Attempts to open a file at the given path.
fn open_file(&self, path: &str, flags: u32) -> Result<fio::FileProxy>;
/// Similar to `open_dir` but returns `None` if the file doesn't exist inside
/// this directory (so avoid using names referring to files under one or more
/// directories).
async fn open_dir_checked(&self, name: &str, flags: u32)
-> Result<Option<fio::DirectoryProxy>>;
/// Attempts to read the max buffer size of a file for a given path.
async fn read_file(&self, path: &str) -> Result<String>;
}
#[async_trait]
impl DirectoryProxyExt for fio::DirectoryProxy {
fn open_dir(&self, path: &str, flags: u32) -> Result<fio::DirectoryProxy> {
let (dir_client, dir_server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>()
.context("creating fidl proxy")?;
self.open(
fio::OPEN_FLAG_DIRECTORY | flags,
fio::MODE_TYPE_DIRECTORY,
path,
fidl::endpoints::ServerEnd::new(dir_server.into_channel()),
)?;
Ok(dir_client)
}
fn open_file(&self, path: &str, flags: u32) -> Result<fio::FileProxy> {
let (file_client, file_server) =
fidl::endpoints::create_proxy::<fio::FileMarker>().context("creating fidl proxy")?;
self.open(
fio::OPEN_FLAG_NOT_DIRECTORY | flags,
fio::MODE_TYPE_FILE,
path,
fidl::endpoints::ServerEnd::new(file_server.into_channel()),
)?;
Ok(file_client)
}
async fn open_dir_checked(
&self,
path: &str,
flags: u32,
) -> Result<Option<fio::DirectoryProxy>> {
if let Some(_) = self.dirent(path).await? {
Ok(Some(self.open_dir(path, flags)?))
} else {
Ok(None)
}
}
async fn read_file(&self, path: &str) -> Result<String> {
Ok(std::str::from_utf8(
self.open_file(path, fio::OPEN_RIGHT_READABLE)
.context(format!("reading file from path: {}", path))?
.read_max_bytes()
.await
.context(format!("reading max buf from file: {}", path))?
.as_ref(),
)?
.to_owned())
}
async fn dirents(&self) -> Result<Vec<Dirent>> {
let mut res = Vec::new();
loop {
let buf = self.dirent_bytes().await?;
if buf.is_empty() {
break;
}
let mut bref: &[u8] = buf.as_ref();
let mut bref_ref = &mut bref;
loop {
let dirent_raw = Dirent::parse::<&[u8], &mut &[u8]>(&mut bref_ref)?;
if dirent_raw.name != "." {
res.push(dirent_raw);
}
if is_empty::<&[u8], &mut &[u8]>(&mut bref_ref) {
break;
}
}
}
Ok(res)
}
async fn dirent(&self, name: &str) -> Result<Option<Dirent>> {
Ok(self.dirents().await?.iter().find(|d| d.name == name).map(|d| d.clone()))
}
}
#[async_trait]
pub trait FileProxyExt {
/// Attempts to read the max bytes for a given file, returning a u8 buffer.
async fn read_max_bytes(&self) -> Result<Vec<u8>>;
}
#[async_trait]
impl FileProxyExt for fio::FileProxy {
async fn read_max_bytes(&self) -> Result<Vec<u8>> {
let (status, buf) = self.read(fio::MAX_BUF).await?;
Status::ok(status).context("read file")?;
Ok(buf)
}
}
type U64 = zerocopy::U64<NetworkEndian>;
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)]
pub enum DirentType {
Unknown(u8),
Directory,
BlockDevice,
File,
Socket,
Service,
}
impl TryFrom<DirentType> for u32 {
type Error = anyhow::Error;
fn try_from(d: DirentType) -> Result<u32> {
Ok(match d {
DirentType::Unknown(t) => return Err(anyhow!("unknown type: {}", t)),
DirentType::Directory => fio::MODE_TYPE_DIRECTORY,
DirentType::BlockDevice => fio::MODE_TYPE_BLOCK_DEVICE,
DirentType::File => fio::MODE_TYPE_FILE,
DirentType::Socket => fio::MODE_TYPE_SOCKET,
DirentType::Service => fio::MODE_TYPE_SERVICE,
})
}
}
impl From<u8> for DirentType {
fn from(dirent_type: u8) -> Self {
match dirent_type {
fio::DIRENT_TYPE_DIRECTORY => DirentType::Directory,
fio::DIRENT_TYPE_BLOCK_DEVICE => DirentType::BlockDevice,
fio::DIRENT_TYPE_FILE => DirentType::File,
fio::DIRENT_TYPE_SOCKET => DirentType::Socket,
fio::DIRENT_TYPE_SERVICE => DirentType::Service,
_ => DirentType::Unknown(dirent_type),
}
}
}
trait ToDirProxy {
/// Attempts to use the root `DirectoryProxy`'s `open` command to open a `DirectoryProxy`.
///
/// Passes `OPEN_FLAG_DIRECTORY` bitwise or'd with `flags` to the `open` command.
fn to_dir_proxy(&self, root: &fio::DirectoryProxy, flags: u32) -> Result<fio::DirectoryProxy>;
}
trait ToFileProxy {
/// Attempts to use the root `DirectoryProxy`'s `open` command to open a `FileProxy`.
///
/// Passes `OPEN_FLAG_NOT_DIRECTORY` bitwise or'd with `flags` to the `open` command.
fn to_file_proxy(&self, root: &fio::DirectoryProxy, flags: u32) -> Result<fio::FileProxy>;
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Dirent {
pub ino: u64,
pub size: u8,
pub dirent_type: DirentType,
pub name: String,
}
impl ToDirProxy for Dirent {
fn to_dir_proxy(&self, root: &fio::DirectoryProxy, flags: u32) -> Result<fio::DirectoryProxy> {
match self.dirent_type {
DirentType::Directory => {
let (dir_client, dir_server) =
fidl::endpoints::create_proxy::<fio::DirectoryMarker>()
.context("creating fidl proxy")?;
root.open(
fio::OPEN_FLAG_DIRECTORY | flags,
self.dirent_type.try_into()?,
&self.name,
fidl::endpoints::ServerEnd::new(dir_server.into_channel()),
)?;
Ok(dir_client)
}
_ => Err(anyhow!("wrong type for dir proxy: {:?}", self.dirent_type)),
}
}
}
impl ToFileProxy for Dirent {
fn to_file_proxy(&self, root: &fio::DirectoryProxy, flags: u32) -> Result<fio::FileProxy> {
match self.dirent_type {
DirentType::Directory | DirentType::Unknown(_) => {
Err(anyhow!("wrong type for file proxy: {:?}", self.dirent_type))
}
_ => {
let (file_client, file_server) = fidl::endpoints::create_proxy::<fio::FileMarker>()
.context("creating fidl proxy")?;
root.open(
fio::OPEN_FLAG_NOT_DIRECTORY | flags,
self.dirent_type.try_into()?,
&self.name,
fidl::endpoints::ServerEnd::new(file_server.into_channel()),
)?;
Ok(file_client)
}
}
}
}
impl Dirent {
fn parse<B: ByteSlice, BV: BufferView<B>>(buffer: &mut BV) -> Result<Self> {
let ino = buffer.take_obj_front::<U64>().context("reading ino")?;
let size = buffer.take_byte_front().context("reading size")?;
let dirent_type: DirentType = buffer.take_byte_front().context("reading type")?.into();
let name = std::str::from_utf8(&buffer.take_front(size.into()).context("reading name")?)
.context("dirent utf8 parsing")?
.to_owned();
Ok(Self { ino: ino.get(), size, dirent_type, name })
}
}
// This is just here to satisfy the trait constraints, otherwise the compiler
// will complain that it doesn't know <B> (if done outside of this function).
// This is sort of the limitation of type inference where it's a pointer to
// a pointer to a pointer.
#[inline]
pub fn is_empty<B: ByteSlice, BV: BufferView<B>>(buffer: &mut BV) -> bool {
buffer.len() == 0
}
/// Contains some fake directories and test harness utilities for constructing
/// unit tests on a fake read-only file system.
#[cfg(test)]
pub mod testing {
use {
fidl_fuchsia_io as fio, fuchsia_zircon_status::Status, futures::TryStreamExt,
std::collections::HashMap,
};
const DIRENT_HEADER_SIZE: usize = 10;
/// The type of a fake dirent.
#[derive(Clone, PartialEq, Debug, Eq)]
pub enum TestDirentTreeType {
/// The file variation of a dirent.
File,
/// The directory variation of a dirent.
Dir,
}
impl From<&TestDirentTreeType> for u8 {
fn from(t: &TestDirentTreeType) -> Self {
match t {
TestDirentTreeType::Dir => fio::DIRENT_TYPE_DIRECTORY,
TestDirentTreeType::File => fio::DIRENT_TYPE_FILE,
}
}
}
/// Represents a whole faked file system.
#[derive(Clone, Debug)]
pub struct TestDirentTree {
/// The name fo the entity.
pub name: String,
/// The dirent type of this entity.
pub ttype: TestDirentTreeType,
/// If this is a file, the contents of the file.
pub contents: String,
/// If this is a directory, the children of this directory.
pub children: Option<HashMap<String, Self>>,
}
/// A builder trait for adding files to a faked file tree.
pub trait TreeBuilder {
/// Adds a file while returning an instance of the builder, allowing
/// for this function to be chained.
fn add_file(self, name: &str, contents: &str) -> Self;
}
impl TreeBuilder for TestDirentTree {
fn add_file(mut self, name: &str, contents: &str) -> Self {
(&mut self).add_file(name, contents);
self
}
}
impl TreeBuilder for &mut TestDirentTree {
fn add_file(self, name: &str, contents: &str) -> Self {
let name = name.to_owned();
let contents = contents.to_owned();
self.children.as_mut().unwrap().insert(
name.clone(),
TestDirentTree { name, ttype: TestDirentTreeType::File, contents, children: None },
);
self
}
}
impl TestDirentTree {
/// Creates a root directory with no name and no children.
pub fn root() -> Self {
Self {
name: "".to_string(),
ttype: TestDirentTreeType::Dir,
contents: "".to_string(),
children: Some(HashMap::new()),
}
}
/// Adds a directory to the tree, and then returns an instance of `Self`
/// so that subsequent commands can be chained to this tree.
pub fn add_dir<'a>(&'a mut self, name: &str) -> &'a mut Self {
let name = name.to_owned();
self.children.as_mut().unwrap().insert(
name.clone(),
TestDirentTree {
name: name.clone(),
ttype: TestDirentTreeType::Dir,
contents: "".to_owned(),
children: Some(HashMap::new()),
},
);
self.children.as_mut().unwrap().get_mut(&name).unwrap()
}
}
fn launch_fake_file(
entity: TestDirentTree,
server: fidl::endpoints::ServerEnd<fio::FileMarker>,
) {
fuchsia_async::Task::local(async move {
let mut stream =
server.into_stream().expect("converting fake file server proxy to stream");
while let Ok(Some(req)) = stream.try_next().await {
match req {
fio::FileRequest::Read { count: _, responder } => {
responder
.send(Status::OK.into_raw(), entity.contents.clone().as_ref())
.expect("writing file test response");
}
_ => panic!("not supported"),
}
}
})
.detach();
}
fn launch_fake_directory(
root: TestDirentTree,
server: fidl::endpoints::ServerEnd<fio::DirectoryMarker>,
) {
fuchsia_async::Task::local(async move {
let mut finished_reading_dirents = false;
let mut stream =
server.into_stream().expect("converting fake dir server proxy to stream");
'stream_loop: while let Ok(Some(req)) = stream.try_next().await {
match req {
fio::DirectoryRequest::Open {
flags: _,
mode,
path,
object,
control_handle: _,
} => {
let mut iter = path.split("/");
let mut child =
match root.children.as_ref().unwrap().get(iter.next().unwrap()) {
Some(child) => child.clone(),
None => {
object.close_with_epitaph(Status::NOT_FOUND).unwrap();
continue;
}
};
for entry in iter {
child = match child.children.as_ref().unwrap().get(entry) {
Some(child) => child.clone(),
None => {
object.close_with_epitaph(Status::NOT_FOUND).unwrap();
continue 'stream_loop;
}
}
}
match child.ttype {
TestDirentTreeType::Dir => {
assert_eq!(mode, fio::MODE_TYPE_DIRECTORY);
launch_fake_directory(
child,
fidl::endpoints::ServerEnd::new(object.into_channel()),
);
}
TestDirentTreeType::File => {
assert_eq!(mode, fio::MODE_TYPE_FILE);
launch_fake_file(
child,
fidl::endpoints::ServerEnd::new(object.into_channel()),
);
}
}
}
fio::DirectoryRequest::ReadDirents { max_bytes: _, responder } => {
let res = if !finished_reading_dirents {
let children = root.children.as_ref().unwrap();
children.values().fold(Vec::with_capacity(256), |mut acc, child| {
let mut buf =
Vec::with_capacity(DIRENT_HEADER_SIZE + child.name.len() + 100);
buf.append(&mut vec![0, 0, 0, 0, 0, 0, 0, 0]);
buf.push(child.name.len() as u8);
buf.push(u8::from(&child.ttype));
// unsafe: this is a test, plus the UTF-8 value is checked later.
buf.append(unsafe { child.name.clone().as_mut_vec() });
acc.append(&mut buf);
acc
})
} else {
vec![]
};
finished_reading_dirents = !finished_reading_dirents;
responder
.send(Status::OK.into_raw(), res.as_ref())
.expect("dirents read test response");
}
_ => panic!("unsupported"),
}
}
})
.detach();
}
/// Launches a fake file system based off of the passed tree.
///
/// Currently the file system is intended to be read-only.
pub fn setup_fake_directory(root: TestDirentTree) -> fio::DirectoryProxy {
let (proxy, server) =
fidl::endpoints::create_proxy::<fio::DirectoryMarker>().expect("making fake dir proxy");
launch_fake_directory(root, server);
proxy
}
}
#[cfg(test)]
pub mod test {
use super::testing::*;
use super::*;
impl Dirent {
/// Creates a default (everything zeroed out) file with the given name.
fn anonymous_file(name: &str) -> Self {
Self { ino: 0, size: 0, dirent_type: DirentType::File, name: name.to_owned() }
}
/// Creates a default (everything zeroed out) directory with the given name.
fn anonymous_dir(name: &str) -> Self {
Self { ino: 0, size: 0, dirent_type: DirentType::Directory, name: name.to_owned() }
}
}
fn take_byte<B: ByteSlice, BV: BufferView<B>>(buffer: &mut BV) {
let _ = buffer.take_byte_front().unwrap();
}
#[test]
fn test_buffer_is_empty() {
let buf: [u8; 1] = [2];
let mut bref = &buf[..];
let mut bref_ref = &mut bref;
assert!(!is_empty::<&[u8], &mut &[u8]>(&mut bref_ref));
take_byte::<&[u8], &mut &[u8]>(&mut bref_ref);
assert!(is_empty::<&[u8], &mut &[u8]>(&mut bref_ref));
}
#[test]
fn test_dirent_to_mode_type() {
let dtype = DirentType::Unknown(252u8);
assert!(u32::try_from(dtype).is_err());
struct Test {
dtype: DirentType,
expected: u32,
}
for test in vec![
Test { dtype: DirentType::Directory, expected: fio::MODE_TYPE_DIRECTORY },
Test { dtype: DirentType::BlockDevice, expected: fio::MODE_TYPE_BLOCK_DEVICE },
Test { dtype: DirentType::File, expected: fio::MODE_TYPE_FILE },
Test { dtype: DirentType::Socket, expected: fio::MODE_TYPE_SOCKET },
Test { dtype: DirentType::Service, expected: fio::MODE_TYPE_SERVICE },
]
.iter()
{
assert_eq!(u32::try_from(test.dtype).unwrap(), test.expected);
}
}
#[test]
fn test_dirent_u8_to_dirent_type() {
struct Test {
u8type: u8,
expected: DirentType,
}
for test in vec![
Test { u8type: fio::DIRENT_TYPE_DIRECTORY, expected: DirentType::Directory },
Test { u8type: fio::DIRENT_TYPE_BLOCK_DEVICE, expected: DirentType::BlockDevice },
Test { u8type: fio::DIRENT_TYPE_FILE, expected: DirentType::File },
Test { u8type: fio::DIRENT_TYPE_SOCKET, expected: DirentType::Socket },
Test { u8type: fio::DIRENT_TYPE_SERVICE, expected: DirentType::Service },
Test { u8type: 253u8, expected: DirentType::Unknown(253u8) },
]
.iter()
{
assert_eq!(DirentType::from(test.u8type), test.expected);
}
}
#[test]
fn test_parse_dirent_raw() -> Result<()> {
#[rustfmt::skip]
let buf = [
// ino
42, 0, 0, 0, 0, 0, 0, 0,
// name length
4,
// type
fio::DIRENT_TYPE_FILE,
// name
't' as u8, 'e' as u8, 's' as u8, 't' as u8,
// ino
41, 0, 0, 0, 0, 0, 0, 1,
// name length
5,
// type
fio::DIRENT_TYPE_SERVICE,
// name
'f' as u8, 'o' as u8, 'o' as u8, 'o' as u8, 'o' as u8,
];
let mut bref = &buf[..];
let mut bref_ref = &mut bref;
let dirent_raw = Dirent::parse::<&[u8], &mut &[u8]>(&mut bref_ref)?;
assert_eq!(dirent_raw.size, 4);
assert_eq!(dirent_raw.ino, 0x2a00000000000000);
assert_eq!(dirent_raw.dirent_type, DirentType::File);
assert_eq!(dirent_raw.name, "test".to_owned());
let dirent_raw = Dirent::parse::<&[u8], &mut &[u8]>(&mut bref_ref)?;
assert_eq!(dirent_raw.size, 5);
assert_eq!(dirent_raw.ino, 0x2900000000000001);
assert_eq!(dirent_raw.dirent_type, DirentType::Service);
assert_eq!(dirent_raw.name, "foooo".to_owned());
Ok(())
}
#[test]
fn test_parse_dirent_raw_malformed() {
#[rustfmt::skip]
let buf = [
// ino
1, 0, 0, 0, 0, 0, 0, 0,
// name length
12,
// type
fio::DIRENT_TYPE_FILE,
// name
'w' as u8, 'o' as u8, 'o' as u8, 'p' as u8, 's' as u8,
];
let mut bref = &buf[..];
let mut bref_ref = &mut bref;
assert!(Dirent::parse::<&[u8], &mut &[u8]>(&mut bref_ref).is_err());
let buf = [1, 2, 3, 4];
let mut bref = &buf[..];
let mut bref_ref = &mut bref;
assert!(Dirent::parse::<&[u8], &mut &[u8]>(&mut bref_ref).is_err());
}
#[test]
fn test_to_proxy_fails() {
let (root, _dir_server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>()
.expect("creating fake root proxy");
let dirent = Dirent::anonymous_dir("foo");
assert!(dirent.to_file_proxy(&root, fio::OPEN_RIGHT_READABLE).is_err());
let dirent = Dirent::anonymous_file("foo");
assert!(dirent.to_dir_proxy(&root, fio::OPEN_RIGHT_READABLE).is_err());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_read_file_from_str() {
let mut root = TestDirentTree::root().add_file("foo", "test").add_file("bar", "other_test");
root.add_dir("baz").add_file("mumble", "testarossa");
let root = setup_fake_directory(root);
let buf = root
.open_file("foo", fio::OPEN_RIGHT_READABLE)
.unwrap()
.read_max_bytes()
.await
.unwrap();
assert_eq!("test", std::str::from_utf8(&buf[..]).unwrap());
let buf = root
.open_file("bar", fio::OPEN_RIGHT_READABLE)
.unwrap()
.read_max_bytes()
.await
.unwrap();
assert_eq!("other_test", std::str::from_utf8(&buf[..]).unwrap());
let buf = root
.open_file("baz/mumble", fio::OPEN_RIGHT_READABLE)
.unwrap()
.read_max_bytes()
.await
.unwrap();
assert_eq!("testarossa", std::str::from_utf8(&buf[..]).unwrap());
assert!(root
.open_file("blorp", fio::OPEN_RIGHT_READABLE)
.unwrap()
.read_max_bytes()
.await
.is_err());
assert!(root
.open_file("blorp/blip/bloop", fio::OPEN_RIGHT_READABLE)
.unwrap()
.read_max_bytes()
.await
.is_err());
}
}