Skip to main content

zerofs_client/
io.rs

1//! `tokio-io` feature: a [`FileCursor`] adapter that turns the positioned
2//! [`File`] API into a stateful `AsyncRead + AsyncWrite + AsyncSeek` stream, so
3//! `tokio::io::copy` and friends work against a ZeroFS file. Rust-only; never
4//! crosses the FFI boundary.
5
6use crate::error::ZeroFsError;
7use crate::file::File;
8use std::future::Future;
9use std::io::{self, SeekFrom};
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{Context, Poll, ready};
13use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
14
15#[cfg(not(target_arch = "wasm32"))]
16type BoxFut<T> = Pin<Box<dyn Future<Output = T> + Send>>;
17#[cfg(target_arch = "wasm32")]
18type BoxFut<T> = Pin<Box<dyn Future<Output = T>>>;
19
20enum State {
21    Idle,
22    Reading(BoxFut<Result<bytes::Bytes, ZeroFsError>>),
23    /// A write in flight, plus how many bytes it covers (added to `pos` on success).
24    Writing {
25        fut: BoxFut<Result<(), ZeroFsError>>,
26        len: u64,
27    },
28    /// A `SeekFrom::End` in flight: resolves the size, then offsets by `delta`.
29    SeekingEnd {
30        fut: BoxFut<Result<u64, ZeroFsError>>,
31        delta: i64,
32    },
33}
34
35/// An owned cursor over a [`File`] implementing `AsyncRead + AsyncWrite +
36/// AsyncSeek`, built entirely on [`File::read_at`]/[`File::write_at`]. Multiple
37/// cursors over one `File` are safe; each carries its own position and the
38/// underlying I/O is positioned, so there is no shared seek pointer.
39///
40/// One operation is in flight at a time per cursor (it is driven by `&mut self`
41/// like every async-io adapter); starting a seek while a read or write is
42/// pending is rejected with `io::ErrorKind::Other`.
43pub struct FileCursor {
44    file: Arc<File>,
45    pos: u64,
46    state: State,
47}
48
49impl FileCursor {
50    pub(crate) fn new(file: Arc<File>) -> Self {
51        Self {
52            file,
53            pos: 0,
54            state: State::Idle,
55        }
56    }
57
58    /// The cursor's current absolute byte offset.
59    pub fn position(&self) -> u64 {
60        self.pos
61    }
62}
63
64impl AsyncRead for FileCursor {
65    fn poll_read(
66        mut self: Pin<&mut Self>,
67        cx: &mut Context<'_>,
68        buf: &mut ReadBuf<'_>,
69    ) -> Poll<io::Result<()>> {
70        loop {
71            match &mut self.state {
72                State::Reading(fut) => {
73                    let data = ready!(fut.as_mut().poll(cx));
74                    self.state = State::Idle;
75                    let data = data.map_err(io::Error::from)?;
76                    let n = data.len().min(buf.remaining());
77                    buf.put_slice(&data[..n]);
78                    self.pos += n as u64;
79                    return Poll::Ready(Ok(()));
80                }
81                State::Idle => {
82                    if buf.remaining() == 0 {
83                        return Poll::Ready(Ok(()));
84                    }
85                    // Cap one read at the negotiated chunk so each poll is a
86                    // single round trip; a partial fill is normal for AsyncRead.
87                    let chunk = self.file.max_read_chunk().max(1) as usize;
88                    let len = buf.remaining().min(chunk) as u32;
89                    let offset = self.pos;
90                    let file = Arc::clone(&self.file);
91                    self.state =
92                        State::Reading(Box::pin(async move { file.read_at(offset, len).await }));
93                }
94                _ => return Poll::Ready(Err(busy())),
95            }
96        }
97    }
98}
99
100impl AsyncWrite for FileCursor {
101    fn poll_write(
102        mut self: Pin<&mut Self>,
103        cx: &mut Context<'_>,
104        buf: &[u8],
105    ) -> Poll<io::Result<usize>> {
106        loop {
107            match &mut self.state {
108                State::Writing { fut, len } => {
109                    let res = ready!(fut.as_mut().poll(cx));
110                    let len = *len;
111                    self.state = State::Idle;
112                    res.map_err(io::Error::from)?;
113                    self.pos += len;
114                    return Poll::Ready(Ok(len as usize));
115                }
116                State::Idle => {
117                    if buf.is_empty() {
118                        return Poll::Ready(Ok(0));
119                    }
120                    // write_at writes all of `data` (chunked internally), so one
121                    // poll consumes the whole buffer.
122                    let offset = self.pos;
123                    let data = buf.to_vec();
124                    let len = data.len() as u64;
125                    let file = Arc::clone(&self.file);
126                    self.state = State::Writing {
127                        fut: Box::pin(async move { file.write_at(offset, &data).await }),
128                        len,
129                    };
130                }
131                _ => return Poll::Ready(Err(busy())),
132            }
133        }
134    }
135
136    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
137        // Drive an in-flight write to completion; there is no client-side write
138        // buffer beyond that (each write_at awaits the server), so once idle
139        // everything sent is acknowledged.
140        match &mut self.state {
141            State::Writing { fut, len } => {
142                let res = ready!(fut.as_mut().poll(cx));
143                let len = *len;
144                self.state = State::Idle;
145                res.map_err(io::Error::from)?;
146                self.pos += len;
147                Poll::Ready(Ok(()))
148            }
149            _ => Poll::Ready(Ok(())),
150        }
151    }
152
153    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
154        self.poll_flush(cx)
155    }
156}
157
158impl AsyncSeek for FileCursor {
159    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
160        if !matches!(self.state, State::Idle) {
161            return Err(busy());
162        }
163        match position {
164            SeekFrom::Start(n) => self.pos = n,
165            SeekFrom::Current(delta) => {
166                self.pos = self
167                    .pos
168                    .checked_add_signed(delta)
169                    .ok_or_else(negative_seek)?;
170            }
171            SeekFrom::End(delta) => {
172                let file = Arc::clone(&self.file);
173                self.state = State::SeekingEnd {
174                    fut: Box::pin(async move { file.metadata().await.map(|m| m.size) }),
175                    delta,
176                };
177            }
178        }
179        Ok(())
180    }
181
182    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
183        if let State::SeekingEnd { fut, delta } = &mut self.state {
184            let size = ready!(fut.as_mut().poll(cx));
185            let delta = *delta;
186            self.state = State::Idle;
187            let size = size.map_err(io::Error::from)?;
188            self.pos = size.checked_add_signed(delta).ok_or_else(negative_seek)?;
189        }
190        Poll::Ready(Ok(self.pos))
191    }
192}
193
194fn busy() -> io::Error {
195    io::Error::other("zerofs FileCursor: another I/O operation is already in flight")
196}
197
198fn negative_seek() -> io::Error {
199    io::Error::new(
200        io::ErrorKind::InvalidInput,
201        "zerofs FileCursor: seek to a negative or out-of-range offset",
202    )
203}