Skip to main content

wasi_common/sync/
dir.rs

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