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