wasi_common/sync/
dir.rs

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