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