Skip to main content

moirai_pal/fs/
file.rs

1use std::fs::{File as StdFile, OpenOptions as StdOpenOptions};
2use std::io::{self, Read, Seek, SeekFrom, Write};
3use std::path::Path;
4
5#[cfg(windows)]
6use std::sync::Mutex;
7
8/// Declarative open-mode configuration for [`File::open_with`].
9///
10/// Named fields keep call sites from transposing modes silently.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct FileOpenOptions {
13    /// Open for reading.
14    pub read: bool,
15    /// Open for writing.
16    pub write: bool,
17    /// Create the file if it does not exist.
18    pub create: bool,
19    /// Append instead of overwriting.
20    pub append: bool,
21    /// Truncate existing content.
22    pub truncate: bool,
23}
24
25impl Default for FileOpenOptions {
26    fn default() -> Self {
27        Self::read_only()
28    }
29}
30
31impl FileOpenOptions {
32    /// Read-only access.
33    #[must_use]
34    pub const fn read_only() -> Self {
35        Self {
36            read: true,
37            write: false,
38            create: false,
39            append: false,
40            truncate: false,
41        }
42    }
43
44    /// Write-only access (creates if absent, truncates existing content).
45    #[must_use]
46    pub const fn write_only() -> Self {
47        Self {
48            read: false,
49            write: true,
50            create: true,
51            append: false,
52            truncate: true,
53        }
54    }
55
56    /// Append access (creates if absent, preserves existing content).
57    #[must_use]
58    pub const fn append_only() -> Self {
59        Self {
60            read: false,
61            write: true,
62            create: true,
63            append: true,
64            truncate: false,
65        }
66    }
67
68    /// Read-write access (creates if absent, preserves existing content).
69    #[must_use]
70    pub const fn read_write() -> Self {
71        Self {
72            read: true,
73            write: true,
74            create: true,
75            append: false,
76            truncate: false,
77        }
78    }
79
80    /// Read-write access that truncates existing content (creates if absent).
81    #[must_use]
82    pub const fn read_write_truncate() -> Self {
83        Self {
84            read: true,
85            write: true,
86            create: true,
87            append: false,
88            truncate: true,
89        }
90    }
91}
92
93/// Platform file handle whose stream and positioned operations all take
94/// `&self`, so one handle can be shared with the threads that run blocking
95/// file I/O.
96///
97/// Every call is a blocking syscall. Async callers run them on a blocking
98/// pool (`moirai_async::fs`), never inside `poll`.
99pub struct File {
100    inner: StdFile,
101    // Windows `seek_read` moves the file cursor, so a positioned read saves and
102    // restores it. Stream operations take the same lock, so no read, write, or
103    // seek observes a positioned read's temporary cursor. Unix `pread` leaves
104    // the cursor alone and needs no lock.
105    #[cfg(windows)]
106    cursor_lock: Mutex<()>,
107}
108
109impl File {
110    /// Open `path` with the modes described by `options`.
111    ///
112    /// # Errors
113    /// Propagates the underlying open error.
114    pub fn open_with<P: AsRef<Path>>(path: P, options: FileOpenOptions) -> io::Result<Self> {
115        let mut opts = StdOpenOptions::new();
116        opts.read(options.read)
117            .write(options.write)
118            .create(options.create)
119            .append(options.append)
120            .truncate(options.truncate);
121        let inner = opts.open(path)?;
122        Ok(Self {
123            inner,
124            #[cfg(windows)]
125            cursor_lock: Mutex::new(()),
126        })
127    }
128
129    /// Run a cursor-moving stream operation, excluding positioned reads on
130    /// Windows.
131    fn with_cursor<T>(&self, operation: impl FnOnce(&StdFile) -> io::Result<T>) -> io::Result<T> {
132        #[cfg(windows)]
133        let _cursor = self
134            .cursor_lock
135            .lock()
136            .map_err(|_| io::Error::other("file cursor lock was poisoned"))?;
137        operation(&self.inner)
138    }
139
140    /// Read into `buf` at the cursor.
141    ///
142    /// # Errors
143    /// Propagates the underlying read error.
144    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
145        self.with_cursor(|mut file| file.read(buf))
146    }
147
148    /// Read from the cursor to the end of the file, appending to `buf`.
149    ///
150    /// # Errors
151    /// Propagates the underlying read error.
152    pub fn read_to_end(&self, buf: &mut Vec<u8>) -> io::Result<usize> {
153        self.with_cursor(|mut file| file.read_to_end(buf))
154    }
155
156    /// Write `buf` at the cursor (or the end, in append mode).
157    ///
158    /// # Errors
159    /// Propagates the underlying write error.
160    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
161        self.with_cursor(|mut file| file.write(buf))
162    }
163
164    /// Write all of `buf` at the cursor (or the end, in append mode).
165    ///
166    /// # Errors
167    /// Propagates the underlying seek or write error.
168    pub fn write_all(&self, buf: &[u8]) -> io::Result<()> {
169        self.with_cursor(|mut file| file.write_all(buf))
170    }
171
172    /// Move the cursor to `pos`.
173    ///
174    /// # Errors
175    /// Propagates the underlying seek error.
176    pub fn seek(&self, pos: SeekFrom) -> io::Result<u64> {
177        self.with_cursor(|mut file| file.seek(pos))
178    }
179
180    /// Synchronize data and metadata to disk.
181    ///
182    /// # Errors
183    /// Propagates the underlying sync error.
184    pub fn sync_all(&self) -> io::Result<()> {
185        self.inner.sync_all()
186    }
187
188    /// Synchronize data (not necessarily metadata) to disk.
189    ///
190    /// # Errors
191    /// Propagates the underlying sync error.
192    pub fn sync_data(&self) -> io::Result<()> {
193        self.inner.sync_data()
194    }
195
196    /// File metadata.
197    ///
198    /// # Errors
199    /// Propagates the underlying metadata error.
200    pub fn metadata(&self) -> io::Result<std::fs::Metadata> {
201        self.inner.metadata()
202    }
203
204    /// Read bytes at an absolute offset without changing the stream cursor.
205    ///
206    /// A successful call may read fewer bytes than requested. Unix provides a
207    /// cursor-independent primitive. Windows `seek_read` changes the cursor,
208    /// so the save/read/restore sequence holds the cursor lock that stream
209    /// operations also take. Targets without a native primitive report
210    /// [`io::ErrorKind::Unsupported`].
211    ///
212    /// # Errors
213    ///
214    /// Propagates the platform read error. On Windows, also reports a poisoned
215    /// positioned-read lock or failure to restore the original cursor.
216    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
217        #[cfg(unix)]
218        {
219            use std::os::unix::fs::FileExt;
220
221            self.inner.read_at(buf, offset)
222        }
223
224        #[cfg(windows)]
225        {
226            use std::os::windows::fs::FileExt;
227
228            let _cursor = self
229                .cursor_lock
230                .lock()
231                .map_err(|_| io::Error::other("file cursor lock was poisoned"))?;
232            let mut file = &self.inner;
233            let original = file.stream_position()?;
234            let read = self.inner.seek_read(buf, offset);
235            file.seek(SeekFrom::Start(original))?;
236            read
237        }
238
239        #[cfg(not(any(unix, windows)))]
240        {
241            let _ = (buf, offset);
242            Err(io::Error::new(
243                io::ErrorKind::Unsupported,
244                "positioned file reads are unsupported on this target",
245            ))
246        }
247    }
248}