| // 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::{DirectoryMarker, DirectoryProxy, MAX_BUF, MODE_TYPE_DIRECTORY}, |
| fuchsia_async::{DurationExt, TimeoutExt}, |
| fuchsia_zircon as zx, |
| futures::future::BoxFuture, |
| 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)] |
| ReadDir(zx::Status), |
| |
| #[error("`unlink` failed with status {:?}", _0)] |
| Unlink(zx::Status), |
| |
| #[error("timeout while reading dir: {}", _0)] |
| Timeout(String), |
| |
| #[error("`rewind` failed with status {:?}", _0)] |
| Rewind(zx::Status), |
| } |
| |
| /// 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 { |
| fidl_fuchsia_io::DIRENT_TYPE_DIRECTORY => DirentKind::Directory, |
| fidl_fuchsia_io::DIRENT_TYPE_BLOCK_DEVICE => DirentKind::BlockDevice, |
| fidl_fuchsia_io::DIRENT_TYPE_FILE => DirentKind::File, |
| fidl_fuchsia_io::DIRENT_TYPE_SOCKET => DirentKind::Socket, |
| fidl_fuchsia_io::DIRENT_TYPE_SERVICE => DirentKind::Service, |
| _ => DirentKind::Unknown, |
| } |
| } |
| } |
| |
| /// A directory entry. |
| #[derive(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 is_dir(&self) -> bool { |
| self.kind == DirentKind::Directory |
| } |
| |
| fn chain(&self, subentry: &DirEntry) -> DirEntry { |
| 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 async fn readdir_recursive( |
| dir: &DirectoryProxy, |
| timeout: Option<zx::Duration>, |
| ) -> Result<Vec<DirEntry>, anyhow::Error> { |
| let mut directories: VecDeque<DirEntry> = VecDeque::new(); |
| let mut entries: Vec<DirEntry> = Vec::new(); |
| |
| // Prime directory queue with immediate descendants. |
| { |
| for entry in readdir(dir).await?.into_iter() { |
| if entry.is_dir() { |
| directories.push_back(entry) |
| } else { |
| entries.push(entry) |
| } |
| } |
| } |
| |
| // Handle a single directory at a time, emitting leaf nodes and queueing up subdirectories for |
| // later iterations. |
| while let Some(entry) = directories.pop_front() { |
| let (subdir, subdir_server) = fidl::endpoints::create_proxy::<DirectoryMarker>()?; |
| let flags = fidl_fuchsia_io::OPEN_FLAG_DIRECTORY | fidl_fuchsia_io::OPEN_RIGHT_READABLE; |
| dir.open( |
| flags, |
| MODE_TYPE_DIRECTORY, |
| &entry.name, |
| ServerEnd::new(subdir_server.into_channel()), |
| ) |
| .map_err(|e| Error::Fidl("open", e))?; |
| |
| let subentries = match timeout { |
| Some(timeout_duration) => { |
| readdir(&subdir) |
| .on_timeout(timeout_duration.after_now(), || { |
| Err(Error::Timeout(entry.name.clone())) |
| }) |
| .await? |
| } |
| None => readdir(&subdir).await?, |
| }; |
| |
| // Emit empty directories as a single entry. |
| if subentries.is_empty() { |
| entries.push(entry); |
| continue; |
| } |
| |
| for subentry in subentries.into_iter() { |
| let subentry = entry.chain(&subentry); |
| if subentry.is_dir() { |
| directories.push_back(subentry) |
| } else { |
| entries.push(subentry) |
| } |
| } |
| } |
| |
| 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. |
| pub async fn readdir(dir: &DirectoryProxy) -> Result<Vec<DirEntry>, Error> { |
| let status = dir.rewind().await.map_err(|e| Error::Fidl("rewind", e))?; |
| zx::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::ok(status).map_err(Error::ReadDir)?; |
| |
| 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) |
| } |
| |
| 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 `fidl_fuchsia_io::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 = fidl_fuchsia_io::OPEN_FLAG_DIRECTORY |
| | fidl_fuchsia_io::OPEN_RIGHT_READABLE |
| | fidl_fuchsia_io::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?; |
| let s = root_dir.unlink(name).await.map_err(|e| Error::Fidl("unlink", e))?; |
| zx::Status::ok(s).map_err(Error::Unlink)?; |
| Ok(()) |
| } |
| |
| // 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?; |
| } |
| _ => {} |
| } |
| let s = dir.unlink(&dirent.name).await.map_err(|e| Error::Fidl("unlink", e))?; |
| zx::Status::ok(s).map_err(Error::Unlink)?; |
| } |
| Ok(()) |
| }; |
| Box::pin(fut) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| fuchsia_async as fasync, |
| fuchsia_vfs_pseudo_fs::{ |
| directory::entry::DirectoryEntry, file::simple::read_only_str, pseudo_directory, |
| }, |
| fuchsia_zircon::DurationNum, |
| io_util, pin_utils, |
| proptest::prelude::*, |
| std::{path::Path, task::Poll}, |
| tempfile::TempDir, |
| }; |
| |
| 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 |
| fidl_fuchsia_io::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 |
| fidl_fuchsia_io::DIRENT_TYPE_FILE, |
| // name (a lonely continuation byte) |
| 0x80, |
| // entry 1 |
| // ino |
| 2, 0, 0, 0, 0, 0, 0, 0, |
| // name length |
| 4, |
| // type |
| fidl_fuchsia_io::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 |
| fidl_fuchsia_io::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, server_end) = fidl::endpoints::create_proxy::<DirectoryMarker>().unwrap(); |
| fasync::spawn(async move { |
| let mut dir = pseudo_directory! { |
| "afile" => read_only_str(|| Ok("".into())), |
| "zzz" => read_only_str(|| Ok("".into())), |
| "subdir" => pseudo_directory! { |
| "ignored" => read_only_str(|| Ok("".into())), |
| }, |
| }; |
| dir.open( |
| fidl_fuchsia_io::OPEN_FLAG_DIRECTORY | fidl_fuchsia_io::OPEN_RIGHT_READABLE, |
| 0, |
| &mut std::iter::empty(), |
| ServerEnd::new(server_end.into_channel()), |
| ); |
| dir.await; |
| unreachable!(); |
| }); |
| |
| // run twice to check that seek offset is properly reset before reading the directory |
| for _ in 0..2 { |
| let entries = readdir(&dir).await.expect("readdir to succeed"); |
| 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_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 entries = |
| readdir_recursive(&dir, None).await.expect("readdir_recursive to succeed"); |
| 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), |
| ] |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_readdir_recursive_timeout() { |
| let mut executor = fasync::Executor::new_with_fake_time().unwrap(); |
| executor.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let fut = async move { |
| let tempdir = TempDir::new().expect("failed to create tmp dir"); |
| let dir = create_nested_dir(&tempdir).await; |
| readdir_recursive(&dir, Some(0.nanos())).await |
| }; |
| |
| pin_utils::pin_mut!(fut); |
| let mut i = 1; |
| let result = loop { |
| executor.wake_main_future(); |
| match executor.run_one_step(&mut fut) { |
| Some(Poll::Ready(x)) => break x, |
| None => panic!("Executor stalled"), |
| Some(Poll::Pending) => { |
| executor.set_fake_time(fasync::Time::from_nanos(10 * i)); |
| i += 1; |
| } |
| } |
| }; |
| |
| match result.as_ref().unwrap_err().downcast_ref::<Error>() { |
| Some(Error::Timeout(_)) => {} |
| _ => panic!(format!("unexpected result: {:?}", result)), |
| } |
| } |
| |
| #[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 to succeed"); |
| let entries = |
| readdir_recursive(&dir, None).await.expect("readdir_recursive to succeed"); |
| 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 to succeed"); |
| let entries = |
| readdir_recursive(&dir, None).await.expect("readdir_recursive to succeed"); |
| 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"), |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE | fidl_fuchsia_io::OPEN_RIGHT_WRITABLE, |
| ) |
| .expect("could not open subdir"); |
| remove_dir_recursive(&subdir, "subsubdir") |
| .await |
| .expect("remove_dir_recursive to succeed"); |
| let entries = |
| readdir_recursive(&dir, None).await.expect("readdir_recursive to succeed"); |
| 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"), |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE | fidl_fuchsia_io::OPEN_RIGHT_WRITABLE, |
| ) |
| .expect("could not open subsubdir"); |
| remove_dir_recursive(&subsubdir, "emptydir") |
| .await |
| .expect("remove_dir_recursive to succeed"); |
| let entries = |
| readdir_recursive(&dir, None).await.expect("readdir_recursive to succeed"); |
| 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::UNAVAILABLE)); |
| 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(), |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE | fidl_fuchsia_io::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), |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE |
| | fidl_fuchsia_io::OPEN_RIGHT_WRITABLE |
| | fidl_fuchsia_io::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); |
| } |
| } |