Skip to main content

wasmtime_wasi/p2/
filesystem.rs

1use crate::TrappableError;
2use crate::filesystem::File;
3use crate::filesystem::sys;
4use crate::p2::bindings::filesystem::types;
5use crate::p2::{InputStream, OutputStream, Pollable, StreamError, StreamResult};
6use crate::runtime::AbortOnDropJoinHandle;
7use bytes::{Bytes, BytesMut};
8use std::io;
9use std::mem;
10use wasmtime::format_err;
11
12pub type FsResult<T> = Result<T, FsError>;
13
14pub type FsError = TrappableError<types::ErrorCode>;
15
16impl From<crate::filesystem::ErrorCode> for types::ErrorCode {
17    fn from(error: crate::filesystem::ErrorCode) -> Self {
18        match error {
19            crate::filesystem::ErrorCode::Access => Self::Access,
20            crate::filesystem::ErrorCode::Already => Self::Already,
21            crate::filesystem::ErrorCode::BadDescriptor => Self::BadDescriptor,
22            crate::filesystem::ErrorCode::Busy => Self::Busy,
23            crate::filesystem::ErrorCode::Exist => Self::Exist,
24            crate::filesystem::ErrorCode::FileTooLarge => Self::FileTooLarge,
25            crate::filesystem::ErrorCode::IllegalByteSequence => Self::IllegalByteSequence,
26            crate::filesystem::ErrorCode::InProgress => Self::InProgress,
27            crate::filesystem::ErrorCode::Interrupted => Self::Interrupted,
28            crate::filesystem::ErrorCode::Invalid => Self::Invalid,
29            crate::filesystem::ErrorCode::Io => Self::Io,
30            crate::filesystem::ErrorCode::IsDirectory => Self::IsDirectory,
31            crate::filesystem::ErrorCode::Loop => Self::Loop,
32            crate::filesystem::ErrorCode::TooManyLinks => Self::TooManyLinks,
33            crate::filesystem::ErrorCode::NameTooLong => Self::NameTooLong,
34            crate::filesystem::ErrorCode::NoEntry => Self::NoEntry,
35            crate::filesystem::ErrorCode::InsufficientMemory => Self::InsufficientMemory,
36            crate::filesystem::ErrorCode::InsufficientSpace => Self::InsufficientSpace,
37            crate::filesystem::ErrorCode::NotDirectory => Self::NotDirectory,
38            crate::filesystem::ErrorCode::NotEmpty => Self::NotEmpty,
39            crate::filesystem::ErrorCode::Unsupported => Self::Unsupported,
40            crate::filesystem::ErrorCode::Overflow => Self::Overflow,
41            crate::filesystem::ErrorCode::NotPermitted => Self::NotPermitted,
42            crate::filesystem::ErrorCode::Pipe => Self::Pipe,
43            crate::filesystem::ErrorCode::InvalidSeek => Self::InvalidSeek,
44        }
45    }
46}
47
48impl From<crate::filesystem::ErrorCode> for FsError {
49    fn from(error: crate::filesystem::ErrorCode) -> Self {
50        types::ErrorCode::from(error).into()
51    }
52}
53
54impl From<wasmtime::component::ResourceTableError> for FsError {
55    fn from(error: wasmtime::component::ResourceTableError) -> Self {
56        Self::trap(error)
57    }
58}
59
60impl From<io::Error> for FsError {
61    fn from(error: io::Error) -> Self {
62        types::ErrorCode::from(error).into()
63    }
64}
65
66pub struct FileInputStream {
67    file: File,
68    position: u64,
69    state: ReadState,
70    max_read_size: usize,
71}
72enum ReadState {
73    Idle,
74    Waiting(AbortOnDropJoinHandle<ReadState>),
75    DataAvailable(Bytes),
76    Error(io::Error),
77    Closed,
78}
79impl FileInputStream {
80    pub fn new(file: &File, position: u64, max_read_size: usize) -> Self {
81        Self {
82            file: file.clone(),
83            position,
84            state: ReadState::Idle,
85            max_read_size,
86        }
87    }
88
89    fn blocking_read(file: &std::fs::File, offset: u64, size: usize) -> ReadState {
90        let mut buf = BytesMut::zeroed(size);
91        loop {
92            match sys::read_at_cursor_unspecified(file, &mut buf, offset) {
93                Ok(0) => return ReadState::Closed,
94                Ok(n) => {
95                    buf.truncate(n);
96                    return ReadState::DataAvailable(buf.freeze());
97                }
98                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
99                    // Try again, continue looping
100                }
101                Err(e) => return ReadState::Error(e),
102            }
103        }
104    }
105
106    /// Wait for existing background task to finish, without starting any new background reads.
107    async fn wait_ready(&mut self) {
108        match &mut self.state {
109            ReadState::Waiting(task) => {
110                self.state = task.await;
111            }
112            _ => {}
113        }
114    }
115}
116#[async_trait::async_trait]
117impl InputStream for FileInputStream {
118    fn read(&mut self, size: usize) -> StreamResult<Bytes> {
119        match &mut self.state {
120            ReadState::Idle => {
121                if size == 0 {
122                    return Ok(Bytes::new());
123                }
124
125                let p = self.position;
126                let size = size.min(self.max_read_size);
127                self.state = ReadState::Waiting(
128                    self.file
129                        .spawn_blocking(move |f| Self::blocking_read(f, p, size)),
130                );
131                Ok(Bytes::new())
132            }
133            ReadState::DataAvailable(b) => {
134                let min_len = b.len().min(size);
135                let chunk = b.split_to(min_len);
136                if b.len() == 0 {
137                    self.state = ReadState::Idle;
138                }
139                self.position += min_len as u64;
140                Ok(chunk)
141            }
142            ReadState::Waiting(_) => Ok(Bytes::new()),
143            ReadState::Error(_) => match mem::replace(&mut self.state, ReadState::Closed) {
144                ReadState::Error(e) => Err(StreamError::LastOperationFailed(e.into())),
145                _ => unreachable!(),
146            },
147            ReadState::Closed => Err(StreamError::Closed),
148        }
149    }
150    /// Specialized blocking_* variant to bypass tokio's task spawning & joining
151    /// overhead on synchronous file I/O.
152    async fn blocking_read(&mut self, size: usize) -> StreamResult<Bytes> {
153        if size == 0 {
154            return self.read(size);
155        }
156        self.wait_ready().await;
157
158        // Before we defer to the regular `read`, make sure it has data ready to go:
159        if let ReadState::Idle = self.state {
160            let p = self.position;
161            let size = size.min(self.max_read_size);
162            self.state = self
163                .file
164                .run_blocking(move |f| Self::blocking_read(f, p, size))
165                .await;
166        }
167
168        self.read(size)
169    }
170    async fn cancel(&mut self) {
171        match mem::replace(&mut self.state, ReadState::Closed) {
172            ReadState::Waiting(task) => {
173                // The task was created using `spawn_blocking`, so unless we're
174                // lucky enough that the task hasn't started yet, the abort
175                // signal won't have any effect and we're forced to wait for it
176                // to run to completion.
177                // From the guest's point of view, `input-stream::drop` then
178                // appears to block. Certainly less than ideal, but arguably still
179                // better than letting the guest rack up an unbounded number of
180                // background tasks. Also, the guest is only blocked if
181                // the stream was dropped mid-read, which we don't expect to
182                // occur frequently.
183                task.cancel().await;
184            }
185            _ => {}
186        }
187    }
188}
189#[async_trait::async_trait]
190impl Pollable for FileInputStream {
191    async fn ready(&mut self) {
192        if let ReadState::Idle = self.state {
193            // The guest hasn't initiated any read, but is nonetheless waiting
194            // for data to be available. We'll start a read for them:
195
196            const DEFAULT_READ_SIZE: usize = 4096;
197            let p = self.position;
198            self.state = ReadState::Waiting(
199                self.file
200                    .spawn_blocking(move |f| Self::blocking_read(f, p, DEFAULT_READ_SIZE)),
201            );
202        }
203
204        self.wait_ready().await
205    }
206}
207
208#[derive(Clone, Copy)]
209pub(crate) enum FileOutputMode {
210    Position(u64),
211    Append,
212}
213
214pub(crate) struct FileOutputStream {
215    file: File,
216    mode: FileOutputMode,
217    state: OutputState,
218    max_write_size: usize,
219}
220
221enum OutputState {
222    Ready,
223    /// Allows join future to be awaited in a cancellable manner. Gone variant indicates
224    /// no task is currently outstanding.
225    Waiting(AbortOnDropJoinHandle<io::Result<usize>>),
226    /// The last I/O operation failed with this error.
227    Error(io::Error),
228    Closed,
229}
230
231impl FileOutputStream {
232    pub fn write_at(file: &File, position: u64, max_write_size: usize) -> Self {
233        Self {
234            file: file.clone(),
235            mode: FileOutputMode::Position(position),
236            state: OutputState::Ready,
237            max_write_size,
238        }
239    }
240
241    pub fn append(file: &File, max_write_size: usize) -> Self {
242        Self {
243            file: file.clone(),
244            mode: FileOutputMode::Append,
245            state: OutputState::Ready,
246            max_write_size,
247        }
248    }
249
250    fn blocking_write(
251        file: &std::fs::File,
252        mut buf: Bytes,
253        mode: FileOutputMode,
254    ) -> io::Result<usize> {
255        match mode {
256            FileOutputMode::Position(mut p) => {
257                let mut total = 0;
258                loop {
259                    let nwritten = sys::write_at_cursor_unspecified(file, buf.as_ref(), p)?;
260                    // afterwards buf contains [nwritten, len):
261                    let _ = buf.split_to(nwritten);
262                    p += nwritten as u64;
263                    total += nwritten;
264                    if buf.is_empty() {
265                        break;
266                    }
267                }
268                Ok(total)
269            }
270            FileOutputMode::Append => {
271                let mut total = 0;
272                loop {
273                    let nwritten = sys::append_cursor_unspecified(file, buf.as_ref())?;
274                    let _ = buf.split_to(nwritten);
275                    total += nwritten;
276                    if buf.is_empty() {
277                        break;
278                    }
279                }
280                Ok(total)
281            }
282        }
283    }
284}
285
286#[async_trait::async_trait]
287impl OutputStream for FileOutputStream {
288    fn write(&mut self, buf: Bytes) -> Result<(), StreamError> {
289        match self.state {
290            OutputState::Ready if buf.len() <= self.max_write_size => {}
291            OutputState::Ready => {
292                return Err(StreamError::Trap(format_err!("write exceeded budget")));
293            }
294            OutputState::Closed => return Err(StreamError::Closed),
295            OutputState::Waiting(_) if buf.is_empty() => return Ok(()),
296            OutputState::Waiting(_) | OutputState::Error(_) => {
297                // a write is pending - this call was not permitted
298                return Err(StreamError::Trap(format_err!(
299                    "write not permitted: check_write not called first"
300                )));
301            }
302        }
303
304        let m = self.mode;
305        self.state = OutputState::Waiting(
306            self.file
307                .spawn_blocking(move |f| Self::blocking_write(f, buf, m)),
308        );
309        Ok(())
310    }
311    /// Specialized blocking_* variant to bypass tokio's task spawning & joining
312    /// overhead on synchronous file I/O.
313    async fn blocking_write_and_flush(&mut self, buf: Bytes) -> StreamResult<()> {
314        self.ready().await;
315
316        match self.state {
317            OutputState::Ready => {}
318            OutputState::Closed => return Err(StreamError::Closed),
319            OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) {
320                OutputState::Error(e) => return Err(StreamError::LastOperationFailed(e.into())),
321                _ => unreachable!(),
322            },
323            OutputState::Waiting(_) => unreachable!("we've just waited for readiness"),
324        }
325
326        let m = self.mode;
327        match self
328            .file
329            .run_blocking(move |f| Self::blocking_write(f, buf, m))
330            .await
331        {
332            Ok(nwritten) => {
333                if let FileOutputMode::Position(p) = &mut self.mode {
334                    *p += nwritten as u64;
335                }
336                self.state = OutputState::Ready;
337                Ok(())
338            }
339            Err(e) => {
340                self.state = OutputState::Closed;
341                Err(StreamError::LastOperationFailed(e.into()))
342            }
343        }
344    }
345    fn flush(&mut self) -> Result<(), StreamError> {
346        match self.state {
347            // Only userland buffering of file writes is in the blocking task,
348            // so there's nothing extra that needs to be done to request a
349            // flush.
350            OutputState::Ready | OutputState::Waiting(_) => Ok(()),
351            OutputState::Closed => Err(StreamError::Closed),
352            OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) {
353                OutputState::Error(e) => Err(StreamError::LastOperationFailed(e.into())),
354                _ => unreachable!(),
355            },
356        }
357    }
358    fn check_write(&mut self) -> Result<usize, StreamError> {
359        match self.state {
360            OutputState::Ready => Ok(self.max_write_size),
361            OutputState::Closed => Err(StreamError::Closed),
362            OutputState::Error(_) => match mem::replace(&mut self.state, OutputState::Closed) {
363                OutputState::Error(e) => Err(StreamError::LastOperationFailed(e.into())),
364                _ => unreachable!(),
365            },
366            OutputState::Waiting(_) => Ok(0),
367        }
368    }
369    async fn cancel(&mut self) {
370        match mem::replace(&mut self.state, OutputState::Closed) {
371            OutputState::Waiting(task) => {
372                // The task was created using `spawn_blocking`, so unless we're
373                // lucky enough that the task hasn't started yet, the abort
374                // signal won't have any effect and we're forced to wait for it
375                // to run to completion.
376                // From the guest's point of view, `output-stream::drop` then
377                // appears to block. Certainly less than ideal, but arguably still
378                // better than letting the guest rack up an unbounded number of
379                // background tasks. Also, the guest is only blocked if
380                // the stream was dropped mid-write, which we don't expect to
381                // occur frequently.
382                task.cancel().await;
383            }
384            _ => {}
385        }
386    }
387}
388
389#[async_trait::async_trait]
390impl Pollable for FileOutputStream {
391    async fn ready(&mut self) {
392        if let OutputState::Waiting(task) = &mut self.state {
393            self.state = match task.await {
394                Ok(nwritten) => {
395                    if let FileOutputMode::Position(p) = &mut self.mode {
396                        *p += nwritten as u64;
397                    }
398                    OutputState::Ready
399                }
400                Err(e) => OutputState::Error(e),
401            };
402        }
403    }
404}
405
406pub struct ReaddirIterator(
407    std::sync::Mutex<Box<dyn Iterator<Item = FsResult<types::DirectoryEntry>> + Send + 'static>>,
408);
409
410impl ReaddirIterator {
411    pub(crate) fn new(
412        i: impl Iterator<Item = FsResult<types::DirectoryEntry>> + Send + 'static,
413    ) -> Self {
414        ReaddirIterator(std::sync::Mutex::new(Box::new(i)))
415    }
416    pub(crate) fn next(&self) -> FsResult<Option<types::DirectoryEntry>> {
417        self.0.lock().unwrap().next().transpose()
418    }
419}
420
421impl IntoIterator for ReaddirIterator {
422    type Item = FsResult<types::DirectoryEntry>;
423    type IntoIter = Box<dyn Iterator<Item = Self::Item> + Send>;
424
425    fn into_iter(self) -> Self::IntoIter {
426        self.0.into_inner().unwrap()
427    }
428}