wasi_cap_std_sync/
dir.rs

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