Skip to main content

vfsi_sync/
io.rs

1use std::io::{self, Read, Seek, SeekFrom as IoSeekFrom, Write};
2use std::path::Path;
3
4use crate::traits::{validate_read_results, validate_write_results};
5use crate::{ReadOp, SeekFrom, VecFs, VfFile, VfOffset};
6
7fn io_error(error: crate::VfError) -> io::Error {
8    let kind = match error.err_no() {
9        crate::ERR_NOENT => io::ErrorKind::NotFound,
10        crate::ERR_EXIST => io::ErrorKind::AlreadyExists,
11        crate::ERR_ACCES => io::ErrorKind::PermissionDenied,
12        crate::ERR_NOTDIR => io::ErrorKind::NotADirectory,
13        crate::ERR_ISDIR => io::ErrorKind::IsADirectory,
14        crate::ERR_INVAL | crate::ERR_EBADF => io::ErrorKind::InvalidInput,
15        _ => io::ErrorKind::Other,
16    };
17    io::Error::new(kind, error)
18}
19
20#[bitfields::bitfield(u8)]
21#[derive(PartialEq, Eq)]
22struct VfOpenFlags {
23    read: bool,
24    write: bool,
25    append: bool,
26    truncate: bool,
27    create: bool,
28    create_new: bool,
29    #[bits(2)]
30    _reserved: u8,
31}
32
33/// Idiomatic Rust open options for a VFSI filesystem.
34#[derive(Clone, Debug)]
35pub struct VfOpenOptions {
36    flags: VfOpenFlags,
37    mode: u32,
38}
39
40impl Default for VfOpenOptions {
41    fn default() -> Self {
42        Self {
43            flags: VfOpenFlags::new(),
44            mode: 0o666,
45        }
46    }
47}
48
49impl VfOpenOptions {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    pub fn read(&mut self, value: bool) -> &mut Self {
55        self.flags.set_read(value);
56        self
57    }
58
59    pub fn write(&mut self, value: bool) -> &mut Self {
60        self.flags.set_write(value);
61        self
62    }
63
64    pub fn append(&mut self, value: bool) -> &mut Self {
65        self.flags.set_append(value);
66        self
67    }
68
69    pub fn truncate(&mut self, value: bool) -> &mut Self {
70        self.flags.set_truncate(value);
71        self
72    }
73
74    pub fn create(&mut self, value: bool) -> &mut Self {
75        self.flags.set_create(value);
76        self
77    }
78
79    pub fn create_new(&mut self, value: bool) -> &mut Self {
80        self.flags.set_create_new(value);
81        self
82    }
83
84    /// Unix permission bits used when a file is created.
85    pub fn mode(&mut self, mode: u32) -> &mut Self {
86        self.mode = mode;
87        self
88    }
89
90    fn flags(&self) -> io::Result<i32> {
91        let writable = self.flags.write() || self.flags.append();
92        let mut flags = match (self.flags.read(), writable) {
93            (true, true) => libc::O_RDWR,
94            (false, true) => libc::O_WRONLY,
95            (true, false) => libc::O_RDONLY,
96            (false, false) => {
97                return Err(io::Error::new(
98                    io::ErrorKind::InvalidInput,
99                    "at least one of read, write, or append must be enabled",
100                ));
101            }
102        };
103        if self.flags.truncate() && !writable {
104            return Err(io::Error::new(
105                io::ErrorKind::InvalidInput,
106                "truncate requires write or append access",
107            ));
108        }
109        if (self.flags.create() || self.flags.create_new()) && !writable {
110            return Err(io::Error::new(
111                io::ErrorKind::InvalidInput,
112                "create requires write or append access",
113            ));
114        }
115        if self.flags.append() {
116            flags |= libc::O_APPEND;
117        }
118        if self.flags.truncate() {
119            flags |= libc::O_TRUNC;
120        }
121        if self.flags.create() || self.flags.create_new() {
122            flags |= libc::O_CREAT;
123        }
124        if self.flags.create_new() {
125            flags |= libc::O_EXCL;
126        }
127        Ok(flags)
128    }
129
130    /// Open a file whose lifetime is tied to the mutable filesystem borrow.
131    pub fn open<'a, F: VecFs + ?Sized>(
132        &self,
133        filesystem: &'a mut F,
134        path: impl AsRef<Path>,
135    ) -> io::Result<VfFileHandle<'a, F>> {
136        let file = filesystem
137            .open(path.as_ref(), self.flags()?, self.mode)
138            .map_err(io_error)?;
139        Ok(VfFileHandle {
140            filesystem,
141            file: Some(file),
142        })
143    }
144}
145
146/// RAII descriptor adapter implementing standard synchronous Rust I/O traits.
147///
148/// Dropping the handle closes the remote descriptor on a best-effort basis.
149/// Use [`close`](Self::close) when a close error must be observed.
150pub struct VfFileHandle<'a, F: VecFs + ?Sized> {
151    filesystem: &'a mut F,
152    file: Option<VfFile>,
153}
154
155impl<F: VecFs + ?Sized> VfFileHandle<'_, F> {
156    /// Descriptor of an open handle. Use [`try_descriptor`](Self::try_descriptor)
157    /// when the handle might already have been closed with `try_close`.
158    pub fn descriptor(&self) -> &VfFile {
159        self.file.as_ref().expect("open handle has a descriptor")
160    }
161
162    pub fn try_descriptor(&self) -> io::Result<&VfFile> {
163        self.file
164            .as_ref()
165            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file handle is closed"))
166    }
167
168    /// Attempt CLOSE while retaining this handle if the backend reports an
169    /// error. A second attempt should follow the backend's recovery policy.
170    pub fn try_close(&mut self) -> io::Result<()> {
171        let Some(file) = self.file.as_ref() else {
172            return Ok(());
173        };
174        self.filesystem.close(file).map_err(io_error)?;
175        self.file = None;
176        Ok(())
177    }
178
179    /// Consume and close the handle. On failure, `Drop` attempts cleanup.
180    pub fn close(mut self) -> io::Result<()> {
181        self.try_close()
182    }
183}
184
185impl<F: VecFs + ?Sized> Read for VfFileHandle<'_, F> {
186    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
187        if buffer.is_empty() {
188            return Ok(0);
189        }
190        let file = self.try_descriptor()?.clone();
191        let requests = [ReadOp::new(file, VfOffset::Cur, buffer.len())];
192        let mut results = self.filesystem.readv(&requests).map_err(io_error)?;
193        validate_read_results("Read::read", &requests, &results).map_err(io_error)?;
194        let result = results.pop().expect("validated one read result");
195        buffer[..result.data.len()].copy_from_slice(&result.data);
196        Ok(result.data.len())
197    }
198}
199
200impl<F: VecFs + ?Sized> Write for VfFileHandle<'_, F> {
201    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
202        if buffer.is_empty() {
203            return Ok(0);
204        }
205        let file = self.try_descriptor()?.clone();
206        let requests = [crate::WriteOpRef::new(&file, VfOffset::Cur, buffer)];
207        let mut results = self
208            .filesystem
209            .writev_borrowed(&requests)
210            .map_err(io_error)?;
211        validate_write_results("Write::write", &requests, &results).map_err(io_error)?;
212        Ok(results.pop().expect("validated one write result").written)
213    }
214
215    fn flush(&mut self) -> io::Result<()> {
216        let file = self.try_descriptor()?.clone();
217        self.filesystem.sync_data(&file).map_err(io_error)
218    }
219}
220
221impl<F: VecFs + ?Sized> Seek for VfFileHandle<'_, F> {
222    fn seek(&mut self, position: IoSeekFrom) -> io::Result<u64> {
223        let file = self.try_descriptor()?.clone();
224        let (offset, whence) = match position {
225            IoSeekFrom::Start(offset) => (
226                i64::try_from(offset).map_err(|_| {
227                    io::Error::new(io::ErrorKind::InvalidInput, "seek offset exceeds i64")
228                })?,
229                SeekFrom::Set,
230            ),
231            IoSeekFrom::End(offset) => (offset, SeekFrom::End),
232            IoSeekFrom::Current(offset) => (offset, SeekFrom::Cur),
233        };
234        let result = self
235            .filesystem
236            .fseek(&file, offset, whence)
237            .map_err(io_error)?;
238        u64::try_from(result)
239            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "negative seek result"))
240    }
241}
242
243impl<F: VecFs + ?Sized> Drop for VfFileHandle<'_, F> {
244    fn drop(&mut self) {
245        if let Some(file) = self.file.take() {
246            let _ = self.filesystem.close(&file);
247        }
248    }
249}
250
251#[cfg(test)]
252mod option_layout_tests {
253    use super::*;
254    #[test]
255    fn packed_open_flags_match_all_access_and_creation_combinations() {
256        assert_eq!(std::mem::size_of::<VfOpenFlags>(), 1);
257        assert_eq!(std::mem::size_of::<VfOpenOptions>(), 8);
258        for bits in 0..64 {
259            let [read, write, append, truncate, create, exclusive] =
260                [0, 1, 2, 3, 4, 5].map(|bit| bits & (1 << bit) != 0);
261            let mut options = VfOpenOptions::new();
262            options
263                .read(read)
264                .write(write)
265                .append(append)
266                .truncate(truncate)
267                .create(create)
268                .create_new(exclusive);
269            let writable = write || append;
270            let invalid = (!read && !writable) || ((truncate || create || exclusive) && !writable);
271            let result = options.flags();
272            if invalid {
273                assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
274                continue;
275            }
276            let mut expected = match (read, writable) {
277                (true, true) => libc::O_RDWR,
278                (false, true) => libc::O_WRONLY,
279                _ => libc::O_RDONLY,
280            };
281            if append {
282                expected |= libc::O_APPEND;
283            }
284            if truncate {
285                expected |= libc::O_TRUNC;
286            }
287            if create || exclusive {
288                expected |= libc::O_CREAT;
289            }
290            if exclusive {
291                expected |= libc::O_EXCL;
292            }
293            assert_eq!(result.unwrap(), expected);
294            options
295                .read(true)
296                .append(false)
297                .truncate(false)
298                .create(false)
299                .create_new(false);
300            assert_eq!(
301                options.flags().unwrap()
302                    & (libc::O_APPEND | libc::O_TRUNC | libc::O_CREAT | libc::O_EXCL),
303                0
304            );
305        }
306    }
307}