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}