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 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 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 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 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 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 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 #[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 .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 #[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}