1 use crate::sync::file::{File, filetype_from};
2 use crate::{
3     Error, ErrorExt,
4     dir::{ReaddirCursor, ReaddirEntity, WasiDir},
5     file::{FdFlags, FileType, Filestat, OFlags},
6 };
7 use cap_fs_ext::{DirEntryExt, DirExt, MetadataExt, OpenOptionsMaybeDirExt, SystemTimeSpec};
8 use cap_std::fs;
9 use std::any::Any;
10 use std::path::{Path, PathBuf};
11 use system_interface::fs::GetSetFdFlags;
12 
13 pub struct Dir(fs::Dir);
14 
15 pub enum OpenResult {
16     File(File),
17     Dir(Dir),
18 }
19 
20 impl Dir {
from_cap_std(dir: fs::Dir) -> Self21     pub fn from_cap_std(dir: fs::Dir) -> Self {
22         Dir(dir)
23     }
24 
open_file_( &self, symlink_follow: bool, path: &str, oflags: OFlags, read: bool, write: bool, fdflags: FdFlags, ) -> Result<OpenResult, Error>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 
rename_(&self, src_path: &str, dest_dir: &Self, dest_path: &str) -> Result<(), Error>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     }
hard_link_( &self, src_path: &str, target_dir: &Self, target_path: &str, ) -> Result<(), Error>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 #[async_trait::async_trait]
124 impl WasiDir for Dir {
as_any(&self) -> &dyn Any125     fn as_any(&self) -> &dyn Any {
126         self
127     }
open_file( &self, symlink_follow: bool, path: &str, oflags: OFlags, read: bool, write: bool, fdflags: FdFlags, ) -> Result<crate::dir::OpenResult, Error>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 
create_dir(&self, path: &str) -> Result<(), Error>144     async fn create_dir(&self, path: &str) -> Result<(), Error> {
145         self.0.create_dir(Path::new(path))?;
146         Ok(())
147     }
readdir( &self, cursor: ReaddirCursor, ) -> Result<Box<dyn Iterator<Item = Result<ReaddirEntity, Error>> + Send>, Error>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 
symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error>230     async fn symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error> {
231         self.0.symlink(src_path, dest_path)?;
232         Ok(())
233     }
remove_dir(&self, path: &str) -> Result<(), Error>234     async fn remove_dir(&self, path: &str) -> Result<(), Error> {
235         self.0.remove_dir(Path::new(path))?;
236         Ok(())
237     }
238 
unlink_file(&self, path: &str) -> Result<(), Error>239     async fn unlink_file(&self, path: &str) -> Result<(), Error> {
240         self.0.remove_file_or_symlink(Path::new(path))?;
241         Ok(())
242     }
read_link(&self, path: &str) -> Result<PathBuf, Error>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     }
get_filestat(&self) -> Result<Filestat, Error>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     }
get_path_filestat( &self, path: &str, follow_symlinks: bool, ) -> Result<Filestat, Error>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     }
rename( &self, src_path: &str, dest_dir: &dyn WasiDir, dest_path: &str, ) -> Result<(), Error>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     }
hard_link( &self, src_path: &str, target_dir: &dyn WasiDir, target_path: &str, ) -> Result<(), Error>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     }
set_times( &self, path: &str, atime: Option<crate::SystemTimeSpec>, mtime: Option<crate::SystemTimeSpec>, follow_symlinks: bool, ) -> Result<(), Error>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 
convert_systimespec(t: Option<crate::SystemTimeSpec>) -> Option<SystemTimeSpec>329 fn 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)]
338 mod test {
339     use super::Dir;
340     use crate::file::{FdFlags, OFlags};
341     use cap_std::ambient_authority;
342     #[test]
scratch_dir()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]
readdir()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 
run<F: std::future::Future>(future: F) -> F::Output425     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!(
436                     "Cannot wait on pending future: must enable wiggle \"async\" future and execute on an async Store"
437                 )
438             }
439         }
440 
441         fn dummy_waker() -> Waker {
442             return unsafe { Waker::from_raw(clone(5 as *const _)) };
443 
444             unsafe fn clone(ptr: *const ()) -> RawWaker {
445                 assert_eq!(ptr as usize, 5);
446                 const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
447                 RawWaker::new(ptr, &VTABLE)
448             }
449 
450             unsafe fn wake(ptr: *const ()) {
451                 assert_eq!(ptr as usize, 5);
452             }
453 
454             unsafe fn wake_by_ref(ptr: *const ()) {
455                 assert_eq!(ptr as usize, 5);
456             }
457 
458             unsafe fn drop(ptr: *const ()) {
459                 assert_eq!(ptr as usize, 5);
460             }
461         }
462     }
463 }
464