Path: blob/main/crates/wasi-common/src/sync/dir.rs
1693 views
use crate::sync::file::{File, filetype_from};1use crate::{2Error, ErrorExt,3dir::{ReaddirCursor, ReaddirEntity, WasiDir},4file::{FdFlags, FileType, Filestat, OFlags},5};6use cap_fs_ext::{DirEntryExt, DirExt, MetadataExt, OpenOptionsMaybeDirExt, SystemTimeSpec};7use cap_std::fs;8use std::any::Any;9use std::path::{Path, PathBuf};10use system_interface::fs::GetSetFdFlags;1112pub struct Dir(fs::Dir);1314pub enum OpenResult {15File(File),16Dir(Dir),17}1819impl Dir {20pub fn from_cap_std(dir: fs::Dir) -> Self {21Dir(dir)22}2324pub fn open_file_(25&self,26symlink_follow: bool,27path: &str,28oflags: OFlags,29read: bool,30write: bool,31fdflags: FdFlags,32) -> Result<OpenResult, Error> {33use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt};3435let mut opts = fs::OpenOptions::new();36opts.maybe_dir(true);3738if oflags.contains(OFlags::CREATE | OFlags::EXCLUSIVE) {39opts.create_new(true);40opts.write(true);41} else if oflags.contains(OFlags::CREATE) {42opts.create(true);43opts.write(true);44}45if oflags.contains(OFlags::TRUNCATE) {46opts.truncate(true);47}48if read {49opts.read(true);50}51if write {52opts.write(true);53} else {54// If not opened write, open read. This way the OS lets us open the file.55// If FileCaps::READ is not set, read calls will be rejected at the56// get_cap check.57opts.read(true);58}59if fdflags.contains(FdFlags::APPEND) {60opts.append(true);61}6263if symlink_follow {64opts.follow(FollowSymlinks::Yes);65} else {66opts.follow(FollowSymlinks::No);67}68// the DSYNC, SYNC, and RSYNC flags are ignored! We do not69// have support for them in cap-std yet.70// ideally OpenOptions would just support this though:71// https://github.com/bytecodealliance/cap-std/issues/14672if fdflags.intersects(73crate::file::FdFlags::DSYNC | crate::file::FdFlags::SYNC | crate::file::FdFlags::RSYNC,74) {75return Err(Error::not_supported().context("SYNC family of FdFlags"));76}7778if oflags.contains(OFlags::DIRECTORY) {79if oflags.contains(OFlags::CREATE)80|| oflags.contains(OFlags::EXCLUSIVE)81|| oflags.contains(OFlags::TRUNCATE)82{83return Err(Error::invalid_argument().context("directory oflags"));84}85}8687let mut f = self.0.open_with(Path::new(path), &opts)?;88if f.metadata()?.is_dir() {89Ok(OpenResult::Dir(Dir::from_cap_std(fs::Dir::from_std_file(90f.into_std(),91))))92} else if oflags.contains(OFlags::DIRECTORY) {93Err(Error::not_dir().context("expected directory but got file"))94} else {95// NONBLOCK does not have an OpenOption either, but we can patch that on with set_fd_flags:96if fdflags.contains(crate::file::FdFlags::NONBLOCK) {97let set_fd_flags = f.new_set_fd_flags(system_interface::fs::FdFlags::NONBLOCK)?;98f.set_fd_flags(set_fd_flags)?;99}100Ok(OpenResult::File(File::from_cap_std(f)))101}102}103104pub fn rename_(&self, src_path: &str, dest_dir: &Self, dest_path: &str) -> Result<(), Error> {105self.0106.rename(Path::new(src_path), &dest_dir.0, Path::new(dest_path))?;107Ok(())108}109pub fn hard_link_(110&self,111src_path: &str,112target_dir: &Self,113target_path: &str,114) -> Result<(), Error> {115let src_path = Path::new(src_path);116let target_path = Path::new(target_path);117self.0.hard_link(src_path, &target_dir.0, target_path)?;118Ok(())119}120}121122#[wiggle::async_trait]123impl WasiDir for Dir {124fn as_any(&self) -> &dyn Any {125self126}127async fn open_file(128&self,129symlink_follow: bool,130path: &str,131oflags: OFlags,132read: bool,133write: bool,134fdflags: FdFlags,135) -> Result<crate::dir::OpenResult, Error> {136let f = self.open_file_(symlink_follow, path, oflags, read, write, fdflags)?;137match f {138OpenResult::File(f) => Ok(crate::dir::OpenResult::File(Box::new(f))),139OpenResult::Dir(d) => Ok(crate::dir::OpenResult::Dir(Box::new(d))),140}141}142143async fn create_dir(&self, path: &str) -> Result<(), Error> {144self.0.create_dir(Path::new(path))?;145Ok(())146}147async fn readdir(148&self,149cursor: ReaddirCursor,150) -> Result<Box<dyn Iterator<Item = Result<ReaddirEntity, Error>> + Send>, Error> {151// We need to keep a full-fidelity io Error around to check for a special failure mode152// on windows, but also this function can fail due to an illegal byte sequence in a153// filename, which we can't construct an io Error to represent.154enum ReaddirError {155Io(std::io::Error),156IllegalSequence,157}158impl From<std::io::Error> for ReaddirError {159fn from(e: std::io::Error) -> ReaddirError {160ReaddirError::Io(e)161}162}163164// cap_std's read_dir does not include . and .., we should prepend these.165// Why does the Ok contain a tuple? We can't construct a cap_std::fs::DirEntry, and we don't166// have enough info to make a ReaddirEntity yet.167let dir_meta = self.0.dir_metadata()?;168let rd = vec![169{170let name = ".".to_owned();171Ok::<_, ReaddirError>((FileType::Directory, dir_meta.ino(), name))172},173{174let name = "..".to_owned();175Ok((FileType::Directory, dir_meta.ino(), name))176},177]178.into_iter()179.chain({180// Now process the `DirEntry`s:181let entries = self.0.entries()?.map(|entry| {182let entry = entry?;183let meta = entry.full_metadata()?;184let inode = meta.ino();185let filetype = filetype_from(&meta.file_type());186let name = entry187.file_name()188.into_string()189.map_err(|_| ReaddirError::IllegalSequence)?;190Ok((filetype, inode, name))191});192193// On Windows, filter out files like `C:\DumpStack.log.tmp` which we194// can't get a full metadata for.195#[cfg(windows)]196let entries = entries.filter(|entry| {197use windows_sys::Win32::Foundation::{198ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION,199};200if let Err(ReaddirError::Io(err)) = entry {201if err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32)202|| err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32)203{204return false;205}206}207true208});209210entries211})212// Enumeration of the iterator makes it possible to define the ReaddirCursor213.enumerate()214.map(|(ix, r)| match r {215Ok((filetype, inode, name)) => Ok(ReaddirEntity {216next: ReaddirCursor::from(ix as u64 + 1),217filetype,218inode,219name,220}),221Err(ReaddirError::Io(e)) => Err(e.into()),222Err(ReaddirError::IllegalSequence) => Err(Error::illegal_byte_sequence()),223})224.skip(u64::from(cursor) as usize);225226Ok(Box::new(rd))227}228229async fn symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error> {230self.0.symlink(src_path, dest_path)?;231Ok(())232}233async fn remove_dir(&self, path: &str) -> Result<(), Error> {234self.0.remove_dir(Path::new(path))?;235Ok(())236}237238async fn unlink_file(&self, path: &str) -> Result<(), Error> {239self.0.remove_file_or_symlink(Path::new(path))?;240Ok(())241}242async fn read_link(&self, path: &str) -> Result<PathBuf, Error> {243let link = self.0.read_link(Path::new(path))?;244Ok(link)245}246async fn get_filestat(&self) -> Result<Filestat, Error> {247let meta = self.0.dir_metadata()?;248Ok(Filestat {249device_id: meta.dev(),250inode: meta.ino(),251filetype: filetype_from(&meta.file_type()),252nlink: meta.nlink(),253size: meta.len(),254atim: meta.accessed().map(|t| Some(t.into_std())).unwrap_or(None),255mtim: meta.modified().map(|t| Some(t.into_std())).unwrap_or(None),256ctim: meta.created().map(|t| Some(t.into_std())).unwrap_or(None),257})258}259async fn get_path_filestat(260&self,261path: &str,262follow_symlinks: bool,263) -> Result<Filestat, Error> {264let meta = if follow_symlinks {265self.0.metadata(Path::new(path))?266} else {267self.0.symlink_metadata(Path::new(path))?268};269Ok(Filestat {270device_id: meta.dev(),271inode: meta.ino(),272filetype: filetype_from(&meta.file_type()),273nlink: meta.nlink(),274size: meta.len(),275atim: meta.accessed().map(|t| Some(t.into_std())).unwrap_or(None),276mtim: meta.modified().map(|t| Some(t.into_std())).unwrap_or(None),277ctim: meta.created().map(|t| Some(t.into_std())).unwrap_or(None),278})279}280async fn rename(281&self,282src_path: &str,283dest_dir: &dyn WasiDir,284dest_path: &str,285) -> Result<(), Error> {286let dest_dir = dest_dir287.as_any()288.downcast_ref::<Self>()289.ok_or(Error::badf().context("failed downcast to cap-std Dir"))?;290self.rename_(src_path, dest_dir, dest_path)291}292async fn hard_link(293&self,294src_path: &str,295target_dir: &dyn WasiDir,296target_path: &str,297) -> Result<(), Error> {298let target_dir = target_dir299.as_any()300.downcast_ref::<Self>()301.ok_or(Error::badf().context("failed downcast to cap-std Dir"))?;302self.hard_link_(src_path, target_dir, target_path)303}304async fn set_times(305&self,306path: &str,307atime: Option<crate::SystemTimeSpec>,308mtime: Option<crate::SystemTimeSpec>,309follow_symlinks: bool,310) -> Result<(), Error> {311if follow_symlinks {312self.0.set_times(313Path::new(path),314convert_systimespec(atime),315convert_systimespec(mtime),316)?;317} else {318self.0.set_symlink_times(319Path::new(path),320convert_systimespec(atime),321convert_systimespec(mtime),322)?;323}324Ok(())325}326}327328fn convert_systimespec(t: Option<crate::SystemTimeSpec>) -> Option<SystemTimeSpec> {329match t {330Some(crate::SystemTimeSpec::Absolute(t)) => Some(SystemTimeSpec::Absolute(t)),331Some(crate::SystemTimeSpec::SymbolicNow) => Some(SystemTimeSpec::SymbolicNow),332None => None,333}334}335336#[cfg(test)]337mod test {338use super::Dir;339use crate::file::{FdFlags, OFlags};340use cap_std::ambient_authority;341#[test]342fn scratch_dir() {343let tempdir = tempfile::Builder::new()344.prefix("cap-std-sync")345.tempdir()346.expect("create temporary dir");347let preopen_dir = cap_std::fs::Dir::open_ambient_dir(tempdir.path(), ambient_authority())348.expect("open ambient temporary dir");349let preopen_dir = Dir::from_cap_std(preopen_dir);350run(crate::WasiDir::open_file(351&preopen_dir,352false,353".",354OFlags::empty(),355false,356false,357FdFlags::empty(),358))359.expect("open the same directory via WasiDir abstraction");360}361362// Readdir does not work on windows, so we won't test it there.363#[cfg(not(windows))]364#[test]365fn readdir() {366use crate::dir::{ReaddirCursor, ReaddirEntity, WasiDir};367use crate::file::{FdFlags, FileType, OFlags};368use std::collections::HashMap;369370fn readdir_into_map(dir: &dyn WasiDir) -> HashMap<String, ReaddirEntity> {371let mut out = HashMap::new();372for readdir_result in373run(dir.readdir(ReaddirCursor::from(0))).expect("readdir succeeds")374{375let entity = readdir_result.expect("readdir entry is valid");376out.insert(entity.name.clone(), entity);377}378out379}380381let tempdir = tempfile::Builder::new()382.prefix("cap-std-sync")383.tempdir()384.expect("create temporary dir");385let preopen_dir = cap_std::fs::Dir::open_ambient_dir(tempdir.path(), ambient_authority())386.expect("open ambient temporary dir");387let preopen_dir = Dir::from_cap_std(preopen_dir);388389let entities = readdir_into_map(&preopen_dir);390assert_eq!(391entities.len(),3922,393"should just be . and .. in empty dir: {entities:?}"394);395assert!(entities.get(".").is_some());396assert!(entities.get("..").is_some());397398run(preopen_dir.open_file(399false,400"file1",401OFlags::CREATE,402true,403false,404FdFlags::empty(),405))406.expect("create file1");407408let entities = readdir_into_map(&preopen_dir);409assert_eq!(entities.len(), 3, "should be ., .., file1 {entities:?}");410assert_eq!(411entities.get(".").expect(". entry").filetype,412FileType::Directory413);414assert_eq!(415entities.get("..").expect(".. entry").filetype,416FileType::Directory417);418assert_eq!(419entities.get("file1").expect("file1 entry").filetype,420FileType::RegularFile421);422}423424fn run<F: std::future::Future>(future: F) -> F::Output {425use std::pin::Pin;426use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};427428let mut f = Pin::from(Box::new(future));429let waker = dummy_waker();430let mut cx = Context::from_waker(&waker);431match f.as_mut().poll(&mut cx) {432Poll::Ready(val) => return val,433Poll::Pending => {434panic!(435"Cannot wait on pending future: must enable wiggle \"async\" future and execute on an async Store"436)437}438}439440fn dummy_waker() -> Waker {441return unsafe { Waker::from_raw(clone(5 as *const _)) };442443unsafe fn clone(ptr: *const ()) -> RawWaker {444assert_eq!(ptr as usize, 5);445const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);446RawWaker::new(ptr, &VTABLE)447}448449unsafe fn wake(ptr: *const ()) {450assert_eq!(ptr as usize, 5);451}452453unsafe fn wake_by_ref(ptr: *const ()) {454assert_eq!(ptr as usize, 5);455}456457unsafe fn drop(ptr: *const ()) {458assert_eq!(ptr as usize, 5);459}460}461}462}463464465