blob: df37819eda159412ce2032dae83a110d5e518437 [file]
// Copyright 2018 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.
#![recursion_limit = "256"]
//! Safe wrappers for enumerating `fuchsia.io.Directory` contents.
use {
fidl::endpoints::ServerEnd,
fidl_fuchsia_io::{self as fio, DirectoryMarker, DirectoryProxy, MAX_BUF, MODE_TYPE_DIRECTORY},
fidl_fuchsia_io2::{UnlinkFlags, UnlinkOptions},
fuchsia_async::{Duration, DurationExt, TimeoutExt},
fuchsia_zircon_status as zx_status,
futures::future::BoxFuture,
futures::stream::{self, BoxStream, StreamExt},
std::{collections::VecDeque, mem, str::Utf8Error},
thiserror::{self, Error},
};
/// Error returned by files_async library.
#[derive(Debug, Error)]
pub enum Error {
#[error("a directory entry could not be decoded: {:?}", _0)]
DecodeDirent(DecodeDirentError),
#[error("fidl error during {}: {:?}", _0, _1)]
Fidl(&'static str, fidl::Error),
#[error("`read_dirents` failed with status {:?}", _0)]
ReadDirents(zx_status::Status),
#[error("`unlink` failed with status {:?}", _0)]
Unlink(zx_status::Status),
#[error("timeout")]
Timeout,
#[error("`rewind` failed with status {:?}", _0)]
Rewind(zx_status::Status),
#[error("Failed to read directory {}: {:?}", name, err)]
ReadDir { name: String, err: anyhow::Error },
}
/// An error encountered while decoding a single directory entry.
#[derive(Debug, PartialEq, Eq, Error)]
pub enum DecodeDirentError {
#[error("an entry extended past the end of the buffer")]
BufferOverrun,
#[error("name is not valid utf-8")]
InvalidUtf8(Utf8Error),
}
/// The type of a node.
#[derive(Eq, Ord, PartialOrd, PartialEq, Clone, Copy, Debug)]
pub enum DirentKind {
Unknown,
Directory,
BlockDevice,
File,
Socket,
Service,
}
impl From<u8> for DirentKind {
fn from(kind: u8) -> Self {
match kind {
fio::DIRENT_TYPE_DIRECTORY => DirentKind::Directory,
fio::DIRENT_TYPE_BLOCK_DEVICE => DirentKind::BlockDevice,
fio::DIRENT_TYPE_FILE => DirentKind::File,
fio::DIRENT_TYPE_SOCKET => DirentKind::Socket,
fio::DIRENT_TYPE_SERVICE => DirentKind::Service,
_ => DirentKind::Unknown,
}
}
}
/// A directory entry.
#[derive(Clone, Eq, Ord, PartialOrd, PartialEq, Debug)]
pub struct DirEntry {
/// The name of this node.
pub name: String,
/// The type of this node, or [`DirentKind::Unknown`] if not known.
pub kind: DirentKind,
}
impl DirEntry {
fn root() -> Self {
Self { name: "".to_string(), kind: DirentKind::Directory }
}
fn is_dir(&self) -> bool {
self.kind == DirentKind::Directory
}
fn is_root(&self) -> bool {
self.is_dir() && self.name.is_empty()
}
fn chain(&self, subentry: &DirEntry) -> DirEntry {
if self.name.is_empty() {
DirEntry { name: subentry.name.clone(), kind: subentry.kind }
} else {
DirEntry { name: format!("{}/{}", self.name, subentry.name), kind: subentry.kind }
}
}
}
/// Returns a Vec of all non-directory nodes and all empty directory nodes in the given directory
/// proxy. The returned entries will not include ".".
/// |timeout| can be provided optionally to specify the maximum time to wait for a directory to be
/// read.
pub fn readdir_recursive(
dir: &DirectoryProxy,
timeout: Option<Duration>,
) -> BoxStream<'_, Result<DirEntry, Error>> {
let mut pending = VecDeque::new();
pending.push_back(DirEntry::root());
let results: VecDeque<DirEntry> = VecDeque::new();
stream::unfold((results, pending), move |(mut results, mut pending)| {
async move {
loop {
// Pending results to stream from the last read directory.
if !results.is_empty() {
let result = results.pop_front().unwrap();
return Some((Ok(result), (results, pending)));
}
// No pending directories to read and per the last condition no pending results to
// stream so finish the stream.
if pending.is_empty() {
return None;
}
// The directory that will be read now.
let dir_entry = pending.pop_front().unwrap();
let (subdir, subdir_server) =
match fidl::endpoints::create_proxy::<DirectoryMarker>() {
Ok((subdir, server)) => (subdir, server),
Err(e) => {
return Some((Err(Error::Fidl("create_proxy", e)), (results, pending)))
}
};
let dir_ref = if dir_entry.is_root() {
dir
} else {
let open_dir_result = dir.open(
fio::OPEN_FLAG_DIRECTORY | fio::OPEN_RIGHT_READABLE,
MODE_TYPE_DIRECTORY,
&dir_entry.name,
ServerEnd::new(subdir_server.into_channel()),
);
if let Err(e) = open_dir_result {
let error = Err(Error::ReadDir {
name: dir_entry.name,
err: anyhow::Error::new(e),
});
return Some((error, (results, pending)));
} else {
&subdir
}
};
let readdir_result = match timeout {
Some(timeout_duration) => readdir_with_timeout(dir_ref, timeout_duration).await,
None => readdir(&dir_ref).await,
};
let subentries = match readdir_result {
Ok(subentries) => subentries,
Err(e) => {
let error = Err(Error::ReadDir {
name: dir_entry.name,
err: anyhow::Error::new(e),
});
return Some((error, (results, pending)));
}
};
// Emit empty directories as a single entry except for the root directory.
if subentries.is_empty() && !dir_entry.name.is_empty() {
return Some((Ok(dir_entry), (results, pending)));
}
for subentry in subentries.into_iter() {
let subentry = dir_entry.chain(&subentry);
if subentry.is_dir() {
pending.push_back(subentry);
} else {
results.push_back(subentry);
}
}
}
}
})
.boxed()
}
/// Returns a sorted Vec of directory entries contained directly in the given directory proxy. The
/// returned entries will not include "." or nodes from any subdirectories.
pub async fn readdir(dir: &DirectoryProxy) -> Result<Vec<DirEntry>, Error> {
let status = dir.rewind().await.map_err(|e| Error::Fidl("rewind", e))?;
zx_status::Status::ok(status).map_err(Error::Rewind)?;
let mut entries = vec![];
loop {
let (status, buf) =
dir.read_dirents(MAX_BUF).await.map_err(|e| Error::Fidl("read_dirents", e))?;
zx_status::Status::ok(status).map_err(Error::ReadDirents)?;
if buf.is_empty() {
break;
}
for entry in parse_dir_entries(&buf) {
let entry = entry.map_err(Error::DecodeDirent)?;
if entry.name != "." {
entries.push(entry);
}
}
}
entries.sort_unstable();
Ok(entries)
}
/// Returns a sorted Vec of directory entries contained directly in the given directory proxy. The
/// returned entries will not include "." or nodes from any subdirectories. Timeouts if the read
/// takes longer than the given `timeout` duration.
pub async fn readdir_with_timeout(
dir: &DirectoryProxy,
timeout: Duration,
) -> Result<Vec<DirEntry>, Error> {
readdir(&dir).on_timeout(timeout.after_now(), || Err(Error::Timeout)).await
}
/// Returns `true` if an entry with the specified name exists in the given directory.
pub async fn dir_contains(dir: &DirectoryProxy, name: &str) -> Result<bool, Error> {
Ok(readdir(&dir).await?.iter().any(|e| e.name == name))
}
/// Returns `true` if an entry with the specified name exists in the given directory.
///
/// Timesout if reading the directory's entries takes longer than the given `timeout`
/// duration.
pub async fn dir_contains_with_timeout(
dir: &DirectoryProxy,
name: &str,
timeout: Duration,
) -> Result<bool, Error> {
Ok(readdir_with_timeout(&dir, timeout).await?.iter().any(|e| e.name == name))
}
/// Parses the buffer returned by a read_dirents FIDL call.
///
/// Returns either an error or a parsed entry for each entry in the supplied buffer (see
/// read_dirents for the format of this buffer).
pub fn parse_dir_entries(mut buf: &[u8]) -> Vec<Result<DirEntry, DecodeDirentError>> {
#[repr(C, packed)]
struct Dirent {
/// The inode number of the entry.
_ino: u64,
/// The length of the filename located after this entry.
size: u8,
/// The type of the entry. One of the `fio::DIRENT_TYPE_*` constants.
kind: u8,
// The unterminated name of the entry. Length is the `size` field above.
// char name[0],
}
const DIRENT_SIZE: usize = mem::size_of::<Dirent>();
let mut entries = vec![];
while !buf.is_empty() {
// Don't read past the end of the buffer.
if DIRENT_SIZE > buf.len() {
entries.push(Err(DecodeDirentError::BufferOverrun));
return entries;
}
// Read the dirent, and figure out how long the name is.
let (head, rest) = buf.split_at(DIRENT_SIZE);
let entry = {
// Cast the dirent bytes into a `Dirent`, and extract out the size of the name and the
// entry type.
let (size, kind) = unsafe {
let dirent: &Dirent = mem::transmute(head.as_ptr());
(dirent.size as usize, dirent.kind)
};
// Don't read past the end of the buffer.
if size > rest.len() {
entries.push(Err(DecodeDirentError::BufferOverrun));
return entries;
}
// Advance to the next entry.
buf = &rest[size..];
match String::from_utf8(rest[..size].to_vec()) {
Ok(name) => Ok(DirEntry { name, kind: kind.into() }),
Err(err) => Err(DecodeDirentError::InvalidUtf8(err.utf8_error())),
}
};
entries.push(entry);
}
entries
}
const DIR_FLAGS: u32 =
fio::OPEN_FLAG_DIRECTORY | fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE;
/// Removes a directory and all of its children. `name` must be a subdirectory of `root_dir`.
///
/// The async analogue of `std::fs::remove_dir_all`.
pub async fn remove_dir_recursive(root_dir: &DirectoryProxy, name: &str) -> Result<(), Error> {
let (dir, dir_server) =
fidl::endpoints::create_proxy::<DirectoryMarker>().expect("failed to create proxy");
root_dir
.open(DIR_FLAGS, MODE_TYPE_DIRECTORY, name, ServerEnd::new(dir_server.into_channel()))
.map_err(|e| Error::Fidl("open", e))?;
remove_dir_contents(dir).await?;
root_dir
.unlink2(
name,
UnlinkOptions { flags: Some(UnlinkFlags::MustBeDirectory), ..UnlinkOptions::EMPTY },
)
.await
.map_err(|e| Error::Fidl("unlink", e))?
.map_err(|s| Error::Unlink(zx_status::Status::from_raw(s)))
}
// Returns a `BoxFuture` instead of being async because async doesn't support recursion.
fn remove_dir_contents(dir: DirectoryProxy) -> BoxFuture<'static, Result<(), Error>> {
let fut = async move {
for dirent in readdir(&dir).await? {
match dirent.kind {
DirentKind::Directory => {
let (subdir, subdir_server) =
fidl::endpoints::create_proxy::<DirectoryMarker>()
.expect("failed to create proxy");
dir.open(
DIR_FLAGS,
MODE_TYPE_DIRECTORY,
&dirent.name,
ServerEnd::new(subdir_server.into_channel()),
)
.map_err(|e| Error::Fidl("open", e))?;
remove_dir_contents(subdir).await?;
}
_ => {}
}
dir.unlink2(&dirent.name, UnlinkOptions::EMPTY)
.await
.map_err(|e| Error::Fidl("unlink", e))?
.map_err(|s| Error::Unlink(zx_status::Status::from_raw(s)))?;
}
Ok(())
};
Box::pin(fut)
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::Context as _,
fidl::endpoints::create_proxy,
fuchsia_async as fasync,
futures::{channel::oneshot, stream::StreamExt},
io_util,
proptest::prelude::*,
std::path::Path,
tempfile::TempDir,
vfs::{
directory::entry::DirectoryEntry, execution_scope::ExecutionScope,
file::vmo::read_only_static, pseudo_directory,
},
};
#[cfg(target_os = "fuchsia")]
use fuchsia_zircon::DurationNum;
#[cfg(target_os = "fuchsia")]
const LONG_DURATION: Duration = Duration::from_seconds(30);
#[cfg(not(target_os = "fuchsia"))]
const LONG_DURATION: Duration = Duration::from_secs(30);
proptest! {
#[test]
fn test_parse_dir_entries_does_not_crash(buf in prop::collection::vec(any::<u8>(), 0..200)) {
parse_dir_entries(&buf);
}
}
#[test]
fn test_parse_dir_entries() {
#[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,
];
assert_eq!(
parse_dir_entries(buf),
vec![Ok(DirEntry { name: "test".to_string(), kind: DirentKind::File })]
);
}
#[test]
fn test_parse_dir_entries_rejects_invalid_utf8() {
#[rustfmt::skip]
let buf = &[
// entry 0
// ino
1, 0, 0, 0, 0, 0, 0, 0,
// name length
1,
// type
fio::DIRENT_TYPE_FILE,
// name (a lonely continuation byte)
0x80,
// entry 1
// ino
2, 0, 0, 0, 0, 0, 0, 0,
// name length
4,
// type
fio::DIRENT_TYPE_FILE,
// name
'o' as u8, 'k' as u8, 'a' as u8, 'y' as u8,
];
let expected_err = std::str::from_utf8(&[0x80]).unwrap_err();
assert_eq!(
parse_dir_entries(buf),
vec![
Err(DecodeDirentError::InvalidUtf8(expected_err)),
Ok(DirEntry { name: "okay".to_string(), kind: DirentKind::File })
]
);
}
#[test]
fn test_parse_dir_entries_overrun() {
#[rustfmt::skip]
let buf = &[
// ino
0, 0, 0, 0, 0, 0, 0, 0,
// name length
5,
// type
fio::DIRENT_TYPE_FILE,
// name
't' as u8, 'e' as u8, 's' as u8, 't' as u8,
];
assert_eq!(parse_dir_entries(buf), vec![Err(DecodeDirentError::BufferOverrun)]);
}
#[fasync::run_singlethreaded(test)]
async fn test_readdir() {
let (dir_client, server_end) = fidl::endpoints::create_proxy::<DirectoryMarker>().unwrap();
let dir = pseudo_directory! {
"afile" => read_only_static(""),
"zzz" => read_only_static(""),
"subdir" => pseudo_directory! {
"ignored" => read_only_static(""),
},
};
let scope = ExecutionScope::new();
dir.open(
scope,
fio::OPEN_FLAG_DIRECTORY | fio::OPEN_RIGHT_READABLE,
0,
vfs::path::Path::empty(),
ServerEnd::new(server_end.into_channel()),
);
// run twice to check that seek offset is properly reset before reading the directory
for _ in 0..2 {
let entries = readdir(&dir_client).await.expect("readdir failed");
assert_eq!(
entries,
vec![
build_direntry("afile", DirentKind::File),
build_direntry("subdir", DirentKind::Directory),
build_direntry("zzz", DirentKind::File),
]
);
}
}
#[fasync::run_singlethreaded(test)]
async fn test_dir_contains() -> Result<(), anyhow::Error> {
let (dir_client, server_end) = fidl::endpoints::create_proxy::<DirectoryMarker>().unwrap();
let dir = pseudo_directory! {
"afile" => read_only_static(""),
"zzz" => read_only_static(""),
"subdir" => pseudo_directory! {
"ignored" => read_only_static(""),
},
};
let scope = ExecutionScope::new();
let () = dir.open(
scope,
fio::OPEN_FLAG_DIRECTORY | fio::OPEN_RIGHT_READABLE,
0,
vfs::path::Path::empty(),
ServerEnd::new(server_end.into_channel()),
);
for file in &["afile", "zzz", "subdir"] {
assert!(dir_contains(&dir_client, file)
.await
.with_context(|| format!("error checking if dir contains {}", file))?);
}
assert!(!dir_contains(&dir_client, "notin")
.await
.context("error checking if dir contains notin")?);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn test_dir_contains_with_timeout() {
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let first = dir_contains_with_timeout(&dir, "notin", LONG_DURATION)
.await
.context("error checking dir contains notin");
let second = dir_contains_with_timeout(&dir, "a", LONG_DURATION)
.await
.context("error checking dir contains a");
matches::assert_matches!((first, second), (Ok(false), Ok(true)));
}
#[fasync::run_singlethreaded(test)]
async fn test_readdir_recursive() {
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
// run twice to check that seek offset is properly reset before reading the directory
for _ in 0..2 {
let (tx, rx) = oneshot::channel();
let clone_dir =
io_util::clone_directory(&dir, fio::CLONE_FLAG_SAME_RIGHTS).expect("clone dir");
fasync::Task::spawn(async move {
let entries = readdir_recursive(&clone_dir, None)
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
tx.send(entries).expect("sending entries failed");
})
.detach();
let entries = rx.await.expect("receiving entries failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("emptydir", DirentKind::Directory),
build_direntry("subdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/emptydir", DirentKind::Directory),
]
);
}
}
#[fasync::run_singlethreaded(test)]
async fn test_readdir_recursive_timeout_expired() {
// This test must use a forever-pending server in order to ensure that the timeout
// triggers before the function under test finishes, even if the timeout is
// in the past.
let (dir, _server) = create_proxy::<DirectoryMarker>().expect("could not create proxy");
let result = readdir_recursive(&dir, Some(0.nanos()))
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>();
assert!(result.is_err());
}
#[fasync::run_singlethreaded(test)]
async fn test_readdir_recursive_timeout() {
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let entries = readdir_recursive(&dir, Some(LONG_DURATION))
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("emptydir", DirentKind::Directory),
build_direntry("subdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/emptydir", DirentKind::Directory),
]
);
}
#[fasync::run_singlethreaded(test)]
async fn test_remove_dir_recursive() {
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
remove_dir_recursive(&dir, "emptydir").await.expect("remove_dir_recursive failed");
let entries = readdir_recursive(&dir, None)
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("subdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/emptydir", DirentKind::Directory),
]
);
}
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
remove_dir_recursive(&dir, "subdir").await.expect("remove_dir_recursive failed");
let entries = readdir_recursive(&dir, None)
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("emptydir", DirentKind::Directory),
]
);
}
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let subdir = io_util::open_directory(
&dir,
&Path::new("subdir"),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
)
.expect("could not open subdir");
remove_dir_recursive(&subdir, "subsubdir").await.expect("remove_dir_recursive failed");
let entries = readdir_recursive(&dir, None)
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("emptydir", DirentKind::Directory),
build_direntry("subdir/a", DirentKind::File),
]
);
}
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let subsubdir = io_util::open_directory(
&dir,
&Path::new("subdir/subsubdir"),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
)
.expect("could not open subsubdir");
remove_dir_recursive(&subsubdir, "emptydir")
.await
.expect("remove_dir_recursive failed");
let entries = readdir_recursive(&dir, None)
.collect::<Vec<Result<DirEntry, Error>>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("readdir_recursive failed");
assert_eq!(
entries,
vec![
build_direntry("a", DirentKind::File),
build_direntry("b", DirentKind::File),
build_direntry("emptydir", DirentKind::Directory),
build_direntry("subdir/a", DirentKind::File),
build_direntry("subdir/subsubdir/a", DirentKind::File),
]
);
}
}
#[fasync::run_singlethreaded(test)]
async fn test_remove_dir_recursive_errors() {
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let res = remove_dir_recursive(&dir, "baddir").await;
let res = res.expect_err("remove_dir did not fail");
match res {
Error::Fidl("rewind", fidl_error) if fidl_error.is_closed() => {}
_ => panic!("unexpected error {:?}", res),
}
}
{
let tempdir = TempDir::new().expect("failed to create tmp dir");
let dir = create_nested_dir(&tempdir).await;
let res = remove_dir_recursive(&dir, ".").await;
let expected: Result<(), Error> = Err(Error::Unlink(zx_status::Status::INVALID_ARGS));
assert_eq!(format!("{:?}", res), format!("{:?}", expected));
}
}
async fn create_nested_dir(tempdir: &TempDir) -> DirectoryProxy {
let dir = io_util::open_directory_in_namespace(
tempdir.path().to_str().unwrap(),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
)
.expect("could not open tmp dir");
io_util::create_sub_directories(&dir, Path::new("emptydir"))
.expect("failed to create emptydir");
io_util::create_sub_directories(&dir, Path::new("subdir/subsubdir/emptydir"))
.expect("failed to create subdir/subsubdir/emptydir");
create_file(&dir, "a").await;
create_file(&dir, "b").await;
create_file(&dir, "subdir/a").await;
create_file(&dir, "subdir/subsubdir/a").await;
dir
}
async fn create_file(dir: &DirectoryProxy, path: &str) {
io_util::open_file(
dir,
Path::new(path),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_CREATE,
)
.expect(&format!("failed to create {}", path));
}
fn build_direntry(name: &str, kind: DirentKind) -> DirEntry {
DirEntry { name: name.to_string(), kind }
}
#[test]
fn test_direntry_is_dir() {
assert!(build_direntry("foo", DirentKind::Directory).is_dir());
// Negative test
assert!(!build_direntry("foo", DirentKind::File).is_dir());
assert!(!build_direntry("foo", DirentKind::Unknown).is_dir());
}
#[test]
fn test_direntry_chaining() {
let parent = build_direntry("foo", DirentKind::Directory);
let child1 = build_direntry("bar", DirentKind::Directory);
let chained1 = parent.chain(&child1);
assert_eq!(&chained1.name, "foo/bar");
assert_eq!(chained1.kind, DirentKind::Directory);
let child2 = build_direntry("baz", DirentKind::File);
let chained2 = parent.chain(&child2);
assert_eq!(&chained2.name, "foo/baz");
assert_eq!(chained2.kind, DirentKind::File);
}
}