Skip to main content

std/os/wasi/
fs.rs

1//! WASIp1-specific extensions to primitives in the [`std::fs`] module.
2//!
3//! [`std::fs`]: crate::fs
4
5#![unstable(feature = "wasi_ext", issue = "71213")]
6
7// Used for `File::read` on intra-doc links
8#[allow(unused_imports)]
9use io::{Read, Write};
10
11#[cfg(target_env = "p1")]
12use crate::ffi::OsStr;
13use crate::fs::{self, File, OpenOptions};
14use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut};
15use crate::macros::rustdoc_skip_body;
16#[cfg(target_env = "p1")]
17use crate::os::fd::AsRawFd;
18use crate::path::Path;
19#[cfg(target_env = "p1")]
20use crate::sys::err2io;
21use crate::sys::{AsInner, AsInnerMut};
22
23/// WASI-specific extensions to [`File`].
24pub trait FileExt {
25    /// Reads a number of bytes starting from a given offset.
26    ///
27    /// Returns the number of bytes read.
28    ///
29    /// The offset is relative to the start of the file and thus independent
30    /// from the current cursor.
31    ///
32    /// The current file cursor is not affected by this function.
33    ///
34    /// Note that similar to [`File::read`], it is not an error to return with a
35    /// short read.
36    fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize>;
37
38    /// Reads a number of bytes starting from a given offset.
39    ///
40    /// Returns the number of bytes read.
41    ///
42    /// The offset is relative to the start of the file and thus independent
43    /// from the current cursor.
44    ///
45    /// The current file cursor is not affected by this function.
46    ///
47    /// Note that similar to [`File::read_vectored`], it is not an error to
48    /// return with a short read.
49    fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize>;
50
51    /// Reads some bytes starting from a given offset into the buffer.
52    ///
53    /// This equivalent to the [`read_at`](FileExt::read_at) method, except that it is passed a
54    /// [`BorrowedCursor`] rather than `&mut [u8]` to allow use with uninitialized buffers. The new
55    /// data will be appended to any existing contents of `buf`.
56    fn read_buf_at(&self, buf: BorrowedCursor<'_, u8>, offset: u64) -> io::Result<()>;
57
58    /// Reads the exact number of byte required to fill `buf` from the given offset.
59    ///
60    /// The offset is relative to the start of the file and thus independent
61    /// from the current cursor.
62    ///
63    /// The current file cursor is not affected by this function.
64    ///
65    /// Similar to [`Read::read_exact`] but uses [`read_at`] instead of `read`.
66    ///
67    /// [`read_at`]: FileExt::read_at
68    ///
69    /// # Errors
70    ///
71    /// If this function encounters an error of the kind
72    /// [`io::ErrorKind::Interrupted`] then the error is ignored and the operation
73    /// will continue.
74    ///
75    /// If this function encounters an "end of file" before completely filling
76    /// the buffer, it returns an error of the kind [`io::ErrorKind::UnexpectedEof`].
77    /// The contents of `buf` are unspecified in this case.
78    ///
79    /// If any other read error is encountered then this function immediately
80    /// returns. The contents of `buf` are unspecified in this case.
81    ///
82    /// If this function returns an error, it is unspecified how many bytes it
83    /// has read, but it will never read more than would be necessary to
84    /// completely fill the buffer.
85    fn read_exact_at(&self, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> {
86        while !buf.is_empty() {
87            match self.read_at(buf, offset) {
88                Ok(0) => break,
89                Ok(n) => {
90                    let tmp = buf;
91                    buf = &mut tmp[n..];
92                    offset += n as u64;
93                }
94                Err(ref e) if e.is_interrupted() => {}
95                Err(e) => return Err(e),
96            }
97        }
98        if !buf.is_empty() { Err(io::Error::READ_EXACT_EOF) } else { Ok(()) }
99    }
100
101    /// Writes a number of bytes starting from a given offset.
102    ///
103    /// Returns the number of bytes written.
104    ///
105    /// The offset is relative to the start of the file and thus independent
106    /// from the current cursor.
107    ///
108    /// The current file cursor is not affected by this function.
109    ///
110    /// When writing beyond the end of the file, the file is appropriately
111    /// extended and the intermediate bytes are initialized with the value 0.
112    ///
113    /// Note that similar to [`File::write`], it is not an error to return a
114    /// short write.
115    fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>;
116
117    /// Writes a number of bytes starting from a given offset.
118    ///
119    /// Returns the number of bytes written.
120    ///
121    /// The offset is relative to the start of the file and thus independent
122    /// from the current cursor.
123    ///
124    /// The current file cursor is not affected by this function.
125    ///
126    /// When writing beyond the end of the file, the file is appropriately
127    /// extended and the intermediate bytes are initialized with the value 0.
128    ///
129    /// Note that similar to [`File::write_vectored`], it is not an error to return a
130    /// short write.
131    fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize>;
132
133    /// Attempts to write an entire buffer starting from a given offset.
134    ///
135    /// The offset is relative to the start of the file and thus independent
136    /// from the current cursor.
137    ///
138    /// The current file cursor is not affected by this function.
139    ///
140    /// This method will continuously call [`write_at`] until there is no more data
141    /// to be written or an error of non-[`io::ErrorKind::Interrupted`] kind is
142    /// returned. This method will not return until the entire buffer has been
143    /// successfully written or such an error occurs. The first error that is
144    /// not of [`io::ErrorKind::Interrupted`] kind generated from this method will be
145    /// returned.
146    ///
147    /// # Errors
148    ///
149    /// This function will return the first error of
150    /// non-[`io::ErrorKind::Interrupted`] kind that [`write_at`] returns.
151    ///
152    /// [`write_at`]: FileExt::write_at
153    fn write_all_at(&self, mut buf: &[u8], mut offset: u64) -> io::Result<()> {
154        while !buf.is_empty() {
155            match self.write_at(buf, offset) {
156                Ok(0) => {
157                    return Err(io::Error::WRITE_ALL_EOF);
158                }
159                Ok(n) => {
160                    buf = &buf[n..];
161                    offset += n as u64
162                }
163                Err(ref e) if e.is_interrupted() => {}
164                Err(e) => return Err(e),
165            }
166        }
167        Ok(())
168    }
169
170    /// Adjusts the flags associated with this file.
171    ///
172    /// This corresponds to the `fd_fdstat_set_flags` syscall.
173    #[doc(alias = "fd_fdstat_set_flags")]
174    #[cfg(target_env = "p1")]
175    fn fdstat_set_flags(&self, flags: u16) -> io::Result<()>;
176
177    /// Adjusts the rights associated with this file.
178    ///
179    /// This corresponds to the `fd_fdstat_set_rights` syscall.
180    #[doc(alias = "fd_fdstat_set_rights")]
181    #[cfg(target_env = "p1")]
182    fn fdstat_set_rights(&self, rights: u64, inheriting: u64) -> io::Result<()>;
183
184    /// Provides file advisory information on a file descriptor.
185    ///
186    /// This corresponds to the `fd_advise` syscall.
187    #[doc(alias = "fd_advise")]
188    #[cfg(target_env = "p1")]
189    fn advise(&self, offset: u64, len: u64, advice: u8) -> io::Result<()>;
190
191    /// Forces the allocation of space in a file.
192    ///
193    /// This corresponds to the `fd_allocate` syscall.
194    #[doc(alias = "fd_allocate")]
195    #[cfg(target_env = "p1")]
196    fn allocate(&self, offset: u64, len: u64) -> io::Result<()>;
197
198    /// Creates a directory.
199    ///
200    /// This corresponds to the `path_create_directory` syscall.
201    #[doc(alias = "path_create_directory")]
202    #[cfg(target_env = "p1")]
203    fn create_directory<P: AsRef<Path>>(&self, dir: P) -> io::Result<()>;
204
205    /// Unlinks a file.
206    ///
207    /// This corresponds to the `path_unlink_file` syscall.
208    #[doc(alias = "path_unlink_file")]
209    #[cfg(target_env = "p1")]
210    fn remove_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()>;
211
212    /// Removes a directory.
213    ///
214    /// This corresponds to the `path_remove_directory` syscall.
215    #[doc(alias = "path_remove_directory")]
216    #[cfg(target_env = "p1")]
217    fn remove_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<()>;
218}
219
220// FIXME: bind fd_fdstat_get - need to define a custom return type
221// FIXME: bind fd_readdir - can't return `ReadDir` since we only have entry name
222// FIXME: bind fd_filestat_set_times maybe? - on crates.io for unix
223// FIXME: bind path_filestat_set_times maybe? - on crates.io for unix
224// FIXME: bind poll_oneoff maybe? - probably should wait for I/O to settle
225// FIXME: bind random_get maybe? - on crates.io for unix
226
227impl FileExt for File {
228    fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
229        self.as_inner().read_at(buf, offset)
230    }
231
232    fn read_buf_at(&self, buf: BorrowedCursor<'_, u8>, offset: u64) -> io::Result<()> {
233        self.as_inner().read_buf_at(buf, offset)
234    }
235
236    #[rustdoc_skip_body]
237    fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
238        self.as_inner().read_vectored_at(bufs, offset)
239    }
240
241    fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
242        self.as_inner().write_at(buf, offset)
243    }
244
245    #[rustdoc_skip_body]
246    fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
247        self.as_inner().write_vectored_at(bufs, offset)
248    }
249
250    #[cfg(target_env = "p1")]
251    fn fdstat_set_flags(&self, flags: u16) -> io::Result<()> {
252        unsafe {
253            wasip1::fd_fdstat_set_flags(self.as_raw_fd() as wasip1::Fd, flags).map_err(err2io)
254        }
255    }
256
257    #[cfg(target_env = "p1")]
258    fn fdstat_set_rights(&self, rights: u64, inheriting: u64) -> io::Result<()> {
259        unsafe {
260            wasip1::fd_fdstat_set_rights(self.as_raw_fd() as wasip1::Fd, rights, inheriting)
261                .map_err(err2io)
262        }
263    }
264
265    #[cfg(target_env = "p1")]
266    fn advise(&self, offset: u64, len: u64, advice: u8) -> io::Result<()> {
267        let advice = match advice {
268            a if a == wasip1::ADVICE_NORMAL.raw() => wasip1::ADVICE_NORMAL,
269            a if a == wasip1::ADVICE_SEQUENTIAL.raw() => wasip1::ADVICE_SEQUENTIAL,
270            a if a == wasip1::ADVICE_RANDOM.raw() => wasip1::ADVICE_RANDOM,
271            a if a == wasip1::ADVICE_WILLNEED.raw() => wasip1::ADVICE_WILLNEED,
272            a if a == wasip1::ADVICE_DONTNEED.raw() => wasip1::ADVICE_DONTNEED,
273            a if a == wasip1::ADVICE_NOREUSE.raw() => wasip1::ADVICE_NOREUSE,
274            _ => {
275                return Err(io::const_error!(
276                    io::ErrorKind::InvalidInput,
277                    "invalid parameter 'advice'",
278                ));
279            }
280        };
281
282        unsafe {
283            wasip1::fd_advise(self.as_raw_fd() as wasip1::Fd, offset, len, advice).map_err(err2io)
284        }
285    }
286
287    #[cfg(target_env = "p1")]
288    fn allocate(&self, offset: u64, len: u64) -> io::Result<()> {
289        unsafe { wasip1::fd_allocate(self.as_raw_fd() as wasip1::Fd, offset, len).map_err(err2io) }
290    }
291
292    #[cfg(target_env = "p1")]
293    fn create_directory<P: AsRef<Path>>(&self, dir: P) -> io::Result<()> {
294        let path = osstr2str(dir.as_ref().as_ref())?;
295        unsafe {
296            wasip1::path_create_directory(self.as_raw_fd() as wasip1::Fd, path).map_err(err2io)
297        }
298    }
299
300    #[cfg(target_env = "p1")]
301    fn remove_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
302        let path = osstr2str(path.as_ref().as_ref())?;
303        unsafe { wasip1::path_unlink_file(self.as_raw_fd() as wasip1::Fd, path).map_err(err2io) }
304    }
305
306    #[cfg(target_env = "p1")]
307    fn remove_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
308        let path = osstr2str(path.as_ref().as_ref())?;
309        unsafe {
310            wasip1::path_remove_directory(self.as_raw_fd() as wasip1::Fd, path).map_err(err2io)
311        }
312    }
313}
314
315/// WASI-specific extensions to [`OpenOptions`].
316pub trait OpenOptionsExt {
317    /// Pass custom flags to the `flags` argument of `open`.
318    fn custom_flags(&mut self, flags: i32) -> &mut Self;
319}
320
321impl OpenOptionsExt for OpenOptions {
322    #[rustdoc_skip_body]
323    fn custom_flags(&mut self, flags: i32) -> &mut OpenOptions {
324        self.as_inner_mut().custom_flags(flags);
325        self
326    }
327}
328
329/// WASI-specific extensions to [`fs::Metadata`].
330pub trait MetadataExt {
331    /// Returns the `st_dev` field of the internal `filestat_t`
332    fn dev(&self) -> u64;
333    /// Returns the `st_ino` field of the internal `filestat_t`
334    fn ino(&self) -> u64;
335    /// Returns the `st_nlink` field of the internal `filestat_t`
336    fn nlink(&self) -> u64;
337}
338
339impl MetadataExt for fs::Metadata {
340    #[rustdoc_skip_body]
341    fn dev(&self) -> u64 {
342        self.as_inner().as_inner().st_dev
343    }
344    #[rustdoc_skip_body]
345    fn ino(&self) -> u64 {
346        self.as_inner().as_inner().st_ino
347    }
348    #[rustdoc_skip_body]
349    fn nlink(&self) -> u64 {
350        self.as_inner().as_inner().st_nlink
351    }
352}
353
354/// WASI-specific extensions for [`fs::FileType`].
355///
356/// Adds support for special WASI file types such as block/character devices,
357/// pipes, and sockets.
358pub trait FileTypeExt {
359    /// Returns `true` if this file type is a block device.
360    fn is_block_device(&self) -> bool;
361    /// Returns `true` if this file type is a character device.
362    fn is_char_device(&self) -> bool;
363    /// Returns `true` if this file type is any type of socket.
364    fn is_socket(&self) -> bool;
365}
366
367impl FileTypeExt for fs::FileType {
368    #[rustdoc_skip_body]
369    fn is_block_device(&self) -> bool {
370        self.as_inner().is(libc::S_IFBLK)
371    }
372    #[rustdoc_skip_body]
373    fn is_char_device(&self) -> bool {
374        self.as_inner().is(libc::S_IFCHR)
375    }
376    #[rustdoc_skip_body]
377    fn is_socket(&self) -> bool {
378        self.as_inner().is(libc::S_IFSOCK)
379    }
380}
381
382/// WASI-specific extension methods for [`fs::DirEntry`].
383pub trait DirEntryExt {
384    /// Returns the underlying `d_ino` field of the `dirent_t`
385    fn ino(&self) -> u64;
386}
387
388impl DirEntryExt for fs::DirEntry {
389    #[rustdoc_skip_body]
390    fn ino(&self) -> u64 {
391        self.as_inner().ino()
392    }
393}
394
395/// Creates a hard link.
396///
397/// This corresponds to the `path_link` syscall.
398#[doc(alias = "path_link")]
399#[cfg(target_env = "p1")]
400pub fn link<P: AsRef<Path>, U: AsRef<Path>>(
401    old_fd: &File,
402    old_flags: u32,
403    old_path: P,
404    new_fd: &File,
405    new_path: U,
406) -> io::Result<()> {
407    unsafe {
408        wasip1::path_link(
409            old_fd.as_raw_fd() as wasip1::Fd,
410            old_flags,
411            osstr2str(old_path.as_ref().as_ref())?,
412            new_fd.as_raw_fd() as wasip1::Fd,
413            osstr2str(new_path.as_ref().as_ref())?,
414        )
415        .map_err(err2io)
416    }
417}
418
419/// Renames a file or directory.
420///
421/// This corresponds to the `path_rename` syscall.
422#[doc(alias = "path_rename")]
423#[cfg(target_env = "p1")]
424pub fn rename<P: AsRef<Path>, U: AsRef<Path>>(
425    old_fd: &File,
426    old_path: P,
427    new_fd: &File,
428    new_path: U,
429) -> io::Result<()> {
430    unsafe {
431        wasip1::path_rename(
432            old_fd.as_raw_fd() as wasip1::Fd,
433            osstr2str(old_path.as_ref().as_ref())?,
434            new_fd.as_raw_fd() as wasip1::Fd,
435            osstr2str(new_path.as_ref().as_ref())?,
436        )
437        .map_err(err2io)
438    }
439}
440
441/// Creates a symbolic link.
442///
443/// This corresponds to the `path_symlink` syscall.
444#[doc(alias = "path_symlink")]
445#[cfg(target_env = "p1")]
446pub fn symlink<P: AsRef<Path>, U: AsRef<Path>>(
447    old_path: P,
448    fd: &File,
449    new_path: U,
450) -> io::Result<()> {
451    unsafe {
452        wasip1::path_symlink(
453            osstr2str(old_path.as_ref().as_ref())?,
454            fd.as_raw_fd() as wasip1::Fd,
455            osstr2str(new_path.as_ref().as_ref())?,
456        )
457        .map_err(err2io)
458    }
459}
460
461/// Creates a symbolic link.
462///
463/// This is a convenience API similar to `std::os::unix::fs::symlink` and
464/// `std::os::windows::fs::symlink_file` and `std::os::windows::fs::symlink_dir`.
465#[cfg_attr(not(test), rustc_diagnostic_item = "fs_symlink_path")]
466pub fn symlink_path<P: AsRef<Path>, U: AsRef<Path>>(old_path: P, new_path: U) -> io::Result<()> {
467    crate::sys::fs::symlink(old_path.as_ref(), new_path.as_ref())
468}
469
470#[cfg(target_env = "p1")]
471fn osstr2str(f: &OsStr) -> io::Result<&str> {
472    f.to_str().ok_or_else(|| io::const_error!(io::ErrorKind::Uncategorized, "input must be utf-8"))
473}