blob: 045e90987d46d6e29b44c9675b4b9d18ebedb5c0 [file] [log] [blame]
// Copyright 2019 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.
//! A helper to build a tree of directory nodes. It is useful in case when a nested tree is
//! desired, with specific nodes to be inserted as the leafs of this tree. It is similar to the
//! functionality provided by the [`pseudo_directory`] macro, except that the macro expects the
//! tree structure to be defined at compile time, while this helper allows the tree structure to be
//! dynamic.
use crate::directory::{self, entry::DirectoryEntry};
use {
fidl_fuchsia_io::MAX_FILENAME,
itertools::Itertools,
std::{collections::HashMap, fmt, marker::PhantomData, slice::Iter},
thiserror::Error,
};
/// Represents a paths provided to [`TreeBuilder::add_entry()`]. See [`TreeBuilder`] for details.
// I think it would be a bit more straightforward to have two different types that implement a
// `Path` trait, `OwnedPath` and `SharedPath`. But, `add_entry` then needs two type variables: one
// for the type of the value passed in, and one for the type of the `Path` trait (either
// `OwnedPath` or `SharedPath`). Type inference fails with two variables requiring explicit type
// annotation. And that defeats the whole purpose of the overloading in the API.
//
// pub fn add_entry<'path, 'components: 'path, F, P: 'path, DE>(
// &mut self,
// path: F,
// entry: DE,
// ) -> Result<(), Error>
//
// Instead we capture the underlying implementation of the path in the `Impl` type and just wrap
// our type around it. `'components` and `AsRef` constraints on the struct itself are not actually
// needed, but it makes it more the usage a bit easier to understand.
pub struct Path<'components, Impl>
where
Impl: AsRef<[&'components str]>,
{
path: Impl,
_components: PhantomData<&'components str>,
}
impl<'components, Impl> Path<'components, Impl>
where
Impl: AsRef<[&'components str]>,
{
fn iter<'path>(&'path self) -> Iter<'path, &'components str>
where
'components: 'path,
{
self.path.as_ref().iter()
}
}
impl<'component> From<&'component str> for Path<'component, Vec<&'component str>> {
fn from(component: &'component str) -> Self {
Path { path: vec![component], _components: PhantomData }
}
}
impl<'components, Impl> From<Impl> for Path<'components, Impl>
where
Impl: AsRef<[&'components str]>,
{
fn from(path: Impl) -> Self {
Path { path, _components: PhantomData }
}
}
impl<'components, Impl> fmt::Display for Path<'components, Impl>
where
Impl: AsRef<[&'components str]>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.iter().format("/"))
}
}
pub enum TreeBuilder<'entries> {
Directory(HashMap<String, TreeBuilder<'entries>>),
Leaf(Box<dyn DirectoryEntry + 'entries>),
}
/// Collects a number of [`DirectoryEntry`] nodes and corresponding paths and the constructs a tree
/// of [`directory::simple::Simple`] directories that hold these nodes. This is a companion tool,
/// related to the [`pseudo_directory!`] macro, except that it is collecting the paths dynamically,
/// while the [`pseudo_directory!`] expects the tree to be specified at compilation time.
///
/// Note that the final tree is build as a result of the [`build()`] method that consumes the
/// builder. You would need to use the [`directory::Simple::add_entry()`] interface to add any new
/// nodes afterwards (a [`directory::controlled::Controller`] APIs).
impl<'entries> TreeBuilder<'entries> {
/// Constructs an empty builder. It is always an empty [`Simple`] directory.
pub fn empty_dir() -> Self {
TreeBuilder::Directory(HashMap::new())
}
/// Adds a [`DirectoryEntry`] at the specified path. It can be either a file or a directory.
/// In case it is a directory, this builder can not add new child nodes inside of the added
/// directory. Any `entry` is treated as an opaque "leaf" as far as the builder is concerned.
///
/// Also see [`add_boxed_entry()`].
pub fn add_entry<'components, P: 'components, PathImpl, DE>(
&mut self,
path: P,
entry: DE,
) -> Result<(), Error>
where
P: Into<Path<'components, PathImpl>>,
PathImpl: AsRef<[&'components str]>,
DE: DirectoryEntry + 'entries,
{
self.add_boxed_entry(path, Box::new(entry))
}
/// Identical to [`add_entry()`] except that the `entry` is [`Box`]ed.
pub fn add_boxed_entry<'components, P: 'components, PathImpl>(
&mut self,
path: P,
entry: Box<dyn DirectoryEntry + 'entries>,
) -> Result<(), Error>
where
P: Into<Path<'components, PathImpl>>,
PathImpl: AsRef<[&'components str]>,
{
let path = path.into();
let traversed = vec![];
let mut rest = path.iter();
match rest.next() {
None => Err(Error::EmptyPath),
Some(name) => self.add_entry_impl(path.iter(), traversed, name, rest, entry),
}
}
fn add_entry_impl<'path, 'components: 'path>(
&mut self,
mut full_path: Iter<'path, &'components str>,
mut traversed: Vec<&'components str>,
name: &'components str,
mut rest: Iter<'path, &'components str>,
entry: Box<dyn DirectoryEntry + 'entries>,
) -> Result<(), Error> {
if name.len() as u64 >= MAX_FILENAME {
return Err(Error::ComponentNameTooLong {
path: full_path.join("/"),
component: name.to_string(),
component_len: name.len(),
max_len: (MAX_FILENAME - 1) as usize,
});
}
if name.contains('/') {
return Err(Error::SlashInComponent {
path: full_path.join("/"),
component: name.to_string(),
});
}
match self {
TreeBuilder::Directory(entries) => match rest.next() {
None => match entries.insert(name.to_string(), TreeBuilder::Leaf(entry)) {
None => Ok(()),
Some(TreeBuilder::Directory(_)) => {
Err(Error::LeafOverDirectory { path: full_path.join("/") })
}
Some(TreeBuilder::Leaf(_)) => {
Err(Error::LeafOverLeaf { path: full_path.join("/") })
}
},
Some(next_component) => {
traversed.push(name);
match entries.get_mut(name) {
None => {
let mut child = TreeBuilder::Directory(HashMap::new());
child.add_entry_impl(
full_path,
traversed,
next_component,
rest,
entry,
)?;
let existing = entries.insert(name.to_string(), child);
assert!(existing.is_none());
Ok(())
}
Some(children) => children.add_entry_impl(
full_path,
traversed,
next_component,
rest,
entry,
),
}
}
},
TreeBuilder::Leaf(_) => Err(Error::EntryInsideLeaf {
path: full_path.join("/"),
traversed: traversed.iter().join("/"),
}),
}
}
/// Consumes the builder, producing a tree with all the nodes provided to [`add_entry()`] at
/// their respective locations. The tree itself is built using [`directory::simple::Simple`]
/// nodes, and the top level is a directory.
pub fn build(self) -> directory::simple::Simple<'entries> {
match self {
TreeBuilder::Directory(mut entries) => {
let mut res = directory::simple::empty();
for (name, child) in entries.drain() {
res.add_boxed_entry(&name, child.build_dyn())
.map_err(|(status, _entry)| format!("Status: {}", status))
.expect(
"Internal error. We have already checked all the entry names. \
There should be no collisions, nor overly long names.",
);
}
res
}
TreeBuilder::Leaf(_) => {
panic!("Leaf nodes should not be buildable through the public API.")
}
}
}
fn build_dyn(self) -> Box<dyn DirectoryEntry + 'entries> {
match self {
TreeBuilder::Directory(mut entries) => {
let mut res = directory::simple::empty();
for (name, child) in entries.drain() {
res.add_boxed_entry(&name, child.build_dyn())
.map_err(|(status, _entry)| format!("Status: {}", status))
.expect(
"Internal error. We have already checked all the entry names. \
There should be no collisions, nor overly long names.",
);
}
Box::new(res)
}
TreeBuilder::Leaf(entry) => entry,
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum Error {
#[error("`add_entry` requires a non-empty path")]
EmptyPath,
#[error(
"Path compoent contains a forward slash.\n\
Path: {}\n\
Component: '{}'",
path,
component
)]
SlashInComponent { path: String, component: String },
#[error(
"Path component name is too long - {} characters. Maximum is {}.\n\
Path: {}\n\
Component: '{}'",
component_len,
max_len,
path,
component
)]
ComponentNameTooLong { path: String, component: String, component_len: usize, max_len: usize },
#[error(
"Trying to insert a leaf over an existing directory.\n\
Path: {}",
path
)]
LeafOverDirectory { path: String },
#[error(
"Trying to overwrite one leaf with another.\n\
Path: {}",
path
)]
LeafOverLeaf { path: String },
#[error(
"Trying to insert an entry inside a leaf.\n\
Leaf path: {}\n\
Path been inserted: {}",
path,
traversed
)]
EntryInsideLeaf { path: String, traversed: String },
}
#[cfg(test)]
mod tests {
use super::{Error, TreeBuilder};
// Macros are exported into the root of the crate.
use crate::{assert_close, assert_read_dirents, open_as_file_assert_content};
use crate::{
directory::{simple, test_utils::run_server_client},
file::simple::read_only_static,
};
use {
fidl_fuchsia_io::{MAX_FILENAME, OPEN_FLAG_DESCRIBE, OPEN_RIGHT_READABLE},
proc_macro_hack::proc_macro_hack,
};
// Create level import of this macro does not affect nested modules. And as attributes can
// only be applied to the whole "use" directive, this need to be present here and need to be
// separate form the above. "use crate::pseudo_directory" generates a warning referring to
// "issue #52234 <https://github.com/rust-lang/rust/issues/52234>".
#[proc_macro_hack(support_nested)]
use fuchsia_vfs_pseudo_fs_macros::pseudo_directory;
#[test]
fn simple() {
let mut tree = TreeBuilder::empty_dir();
tree.add_entry("a", read_only_static("A content")).unwrap();
tree.add_entry("b", read_only_static("B content")).unwrap();
let root = tree.build();
run_server_client(OPEN_RIGHT_READABLE, root, |root| async move {
assert_read_dirents_one_listing!(
root, 1000,
{ DIRECTORY, b"." },
{ FILE, b"a" },
{ FILE, b"b" },
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"a",
"A content"
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"b",
"B content"
);
assert_close!(root);
});
}
#[test]
fn overlapping_paths() {
let mut tree = TreeBuilder::empty_dir();
tree.add_entry(&["one", "two"], read_only_static("A")).unwrap();
tree.add_entry(&["one", "three"], read_only_static("B")).unwrap();
tree.add_entry("four", read_only_static("C")).unwrap();
let root = tree.build();
run_server_client(OPEN_RIGHT_READABLE, root, |root| async move {
assert_read_dirents_one_listing!(
root, 1000,
{ DIRECTORY, b"." },
{ FILE, b"four" },
{ DIRECTORY, b"one" },
);
assert_read_dirents_path_one_listing!(
&root, "one", 1000,
{ DIRECTORY, b"." },
{ FILE, b"three" },
{ FILE, b"two" },
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"one/two",
"A"
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"one/three",
"B"
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"four",
"C"
);
assert_close!(root);
});
}
#[test]
fn directory_leaf() {
let etc = pseudo_directory! {
"fstab" => read_only_static("/dev/fs /"),
"ssh" => pseudo_directory! {
"sshd_config" => read_only_static("# Empty"),
},
};
let mut tree = TreeBuilder::empty_dir();
tree.add_entry("etc", etc).unwrap();
tree.add_entry("uname", read_only_static("Fuchsia")).unwrap();
let root = tree.build();
run_server_client(OPEN_RIGHT_READABLE, root, |root| async move {
assert_read_dirents_one_listing!(
root, 1000,
{ DIRECTORY, b"." },
{ DIRECTORY, b"etc" },
{ FILE, b"uname" },
);
assert_read_dirents_path_one_listing!(
&root, "etc", 1000,
{ DIRECTORY, b"." },
{ FILE, b"fstab" },
{ DIRECTORY, b"ssh" },
);
assert_read_dirents_path_one_listing!(
&root, "etc/ssh", 1000,
{ DIRECTORY, b"." },
{ FILE, b"sshd_config" },
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"etc/fstab",
"/dev/fs /"
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"etc/ssh/sshd_config",
"# Empty"
);
open_as_file_assert_content!(
&root,
OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE,
"uname",
"Fuchsia"
);
assert_close!(root);
});
}
#[test]
fn error_empty_path_in_add_entry() {
let mut tree = TreeBuilder::empty_dir();
let err = tree
.add_entry(vec![], read_only_static("Invalid"))
.expect_err("Empty paths are not allowed.");
assert_eq!(err, Error::EmptyPath);
}
#[test]
fn error_slash_in_compoenent() {
let mut tree = TreeBuilder::empty_dir();
let err = tree
.add_entry("a/b", read_only_static("Invalid"))
.expect_err("Slash in path compoenent name.");
assert_eq!(
err,
Error::SlashInComponent { path: "a/b".to_string(), component: "a/b".to_string() }
);
}
#[test]
fn error_slash_in_second_compoenent() {
let mut tree = TreeBuilder::empty_dir();
let err = tree
.add_entry(&["a", "b/c"], read_only_static("Invalid"))
.expect_err("Slash in path compoenent name.");
assert_eq!(
err,
Error::SlashInComponent { path: "a/b/c".to_string(), component: "b/c".to_string() }
);
}
#[test]
fn error_component_name_too_long() {
let mut tree = TreeBuilder::empty_dir();
let long_component = "abcdefghij".repeat(MAX_FILENAME as usize / 10 + 1);
let path: &[&str] = &["a", &long_component, "b"];
let err = tree
.add_entry(path, read_only_static("Invalid"))
.expect_err("Individual component names may not exceed MAX_FILENAME bytes.");
assert_eq!(
err,
Error::ComponentNameTooLong {
path: format!("a/{}/b", long_component),
component: long_component.clone(),
component_len: long_component.len(),
max_len: (MAX_FILENAME - 1) as usize,
}
);
}
#[test]
fn error_leaf_over_directory() {
let mut tree = TreeBuilder::empty_dir();
tree.add_entry(&["top", "nested", "file"], read_only_static("Content")).unwrap();
let err = tree
.add_entry(&["top", "nested"], read_only_static("Invalid"))
.expect_err("A leaf may not be constructed over a directory.");
assert_eq!(err, Error::LeafOverDirectory { path: "top/nested".to_string() });
}
#[test]
fn error_leaf_over_leaf() {
let mut tree = TreeBuilder::empty_dir();
tree.add_entry(&["top", "nested", "file"], read_only_static("Content")).unwrap();
let err = tree
.add_entry(&["top", "nested", "file"], read_only_static("Invalid"))
.expect_err("A leaf may not be constructed over another leaf.");
assert_eq!(err, Error::LeafOverLeaf { path: "top/nested/file".to_string() });
}
#[test]
fn error_entry_inside_leaf() {
let mut tree = TreeBuilder::empty_dir();
tree.add_entry(&["top", "file"], read_only_static("Content")).unwrap();
let err = tree
.add_entry(&["top", "file", "nested"], read_only_static("Invalid"))
.expect_err("A leaf may not be constructed over another leaf.");
assert_eq!(
err,
Error::EntryInsideLeaf {
path: "top/file/nested".to_string(),
traversed: "top/file".to_string()
}
);
}
#[test]
fn error_entry_inside_leaf_directory() {
let mut tree = TreeBuilder::empty_dir();
// Even when a leaf is itself a directory the tree builder can not insert a nested entry.
tree.add_entry(&["top", "file"], simple::empty()).unwrap();
let err = tree
.add_entry(&["top", "file", "nested"], read_only_static("Invalid"))
.expect_err("A leaf may not be constructed over another leaf.");
assert_eq!(
err,
Error::EntryInsideLeaf {
path: "top/file/nested".to_string(),
traversed: "top/file".to_string()
}
);
}
}