modules/fs/
handle.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::cell::RefCell;
8use std::fmt::{Display, Formatter};
9use std::fs::File;
10use std::future::{Future, poll_fn};
11use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
12use std::rc::Rc;
13use std::str::FromStr;
14use std::sync::Arc;
15use std::task::Poll;
16use std::{io, slice};
17
18use ion::class::Reflector;
19use ion::conversions::{ConversionBehavior, FromValue, IntoValue, ToValue as _};
20use ion::function::Opt;
21use ion::typedarray::{Uint8Array, Uint8ArrayWrapper};
22use ion::{Context, Error, ErrorKind, Promise, Result, TracedHeap, Value, js_class};
23use mozjs::jsval::DoubleValue;
24use runtime::globals::file::BufferSource;
25use runtime::promise::future_to_promise;
26use tokio::task::spawn_blocking;
27
28use crate::fs::{Metadata, file_error, seek_error};
29
30#[derive(Copy, Clone, Debug, Default)]
31pub enum SeekMode {
32	#[default]
33	Current,
34	Start,
35	End,
36}
37
38impl FromStr for SeekMode {
39	type Err = Error;
40
41	fn from_str(redirect: &str) -> Result<SeekMode> {
42		use SeekMode as SM;
43		match redirect {
44			"current" => Ok(SM::Current),
45			"start" => Ok(SM::Start),
46			"end" => Ok(SM::End),
47			_ => Err(Error::new("Invalid value for Enumeration SeekMode", ErrorKind::Type)),
48		}
49	}
50}
51
52impl Display for SeekMode {
53	fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
54		use SeekMode as SM;
55		match self {
56			SM::Current => write!(f, "current"),
57			SM::Start => write!(f, "start"),
58			SM::End => write!(f, "end"),
59		}
60	}
61}
62
63impl<'cx> FromValue<'cx> for SeekMode {
64	type Config = ();
65
66	fn from_value(cx: &'cx Context, value: &Value, _: bool, _: ()) -> Result<SeekMode> {
67		let redirect = String::from_value(cx, value, true, ())?;
68		SeekMode::from_str(&redirect)
69	}
70}
71
72pub enum ReadResult {
73	BytesWritten(usize),
74	Buffer(Vec<u8>),
75}
76
77impl IntoValue<'_> for ReadResult {
78	fn into_value(self: Box<Self>, cx: &Context, value: &mut Value) {
79		match *self {
80			ReadResult::BytesWritten(bytes) => value.handle_mut().set(DoubleValue(bytes as f64)),
81			ReadResult::Buffer(buffer) => Uint8ArrayWrapper::from(buffer).into_typed_array(cx).to_value(cx, value),
82		}
83	}
84}
85
86#[js_class]
87pub struct FileHandle {
88	reflector: Reflector,
89	#[trace(no_trace)]
90	path: Arc<str>,
91	#[trace(no_trace)]
92	handle: Rc<RefCell<Option<File>>>,
93}
94
95impl FileHandle {
96	pub(crate) fn new(path: &str, file: File) -> FileHandle {
97		FileHandle {
98			reflector: Reflector::new(),
99			path: Arc::from(path),
100			handle: Rc::new(RefCell::new(Some(file))),
101		}
102	}
103
104	pub(crate) fn with_sync<F, T>(&self, callback: F) -> Result<T>
105	where
106		F: FnOnce(&mut File) -> Result<T>,
107	{
108		match &mut *self.handle.borrow_mut() {
109			Some(handle) => callback(handle),
110			None => Err(Error::new("File is busy due to async operation.", None)),
111		}
112	}
113
114	pub(crate) fn with_blocking_task<F, T>(&self, callback: F) -> impl Future<Output = io::Result<T>> + 'static
115	where
116		F: FnOnce(&mut File) -> io::Result<T> + Send + 'static,
117		T: Send + 'static,
118	{
119		let handle_cell = Rc::clone(&self.handle);
120
121		async move {
122			let mut taken = false;
123			let mut handle = poll_fn(|wcx| {
124				if let Ok(mut handle) = handle_cell.try_borrow_mut()
125					&& let Some(file) = handle.as_mut()
126				{
127					let file = file.try_clone().ok().unwrap_or_else(|| {
128						taken = true;
129						handle.take().unwrap()
130					});
131					return Poll::Ready(file);
132				}
133
134				wcx.waker().wake_by_ref();
135				Poll::Pending
136			})
137			.await;
138
139			let (handle, result) = spawn_blocking(move || {
140				let result = callback(&mut handle);
141				(handle, result)
142			})
143			.await
144			.unwrap();
145
146			if taken {
147				handle_cell.borrow_mut().replace(handle);
148			}
149
150			result
151		}
152	}
153
154	#[expect(clippy::too_many_arguments)]
155	pub(crate) fn with_blocking_promise<'cx, F, T, A, E, D>(
156		&self, cx: &'cx Context, action: &'static str, path: Arc<str>, callback: F, callback_after: A,
157		error_callback: E, error_data: D,
158	) -> Option<Promise<'cx>>
159	where
160		F: FnOnce(&mut File) -> io::Result<T> + Send + 'static,
161		T: for<'cx2> IntoValue<'cx2> + Send + 'static,
162		A: FnOnce() + 'static,
163		E: for<'p, 'e> FnOnce(&'static str, &'p str, &'e io::Error, D) -> Error + 'static,
164		D: 'static,
165	{
166		let task = self.with_blocking_task(callback);
167		future_to_promise(cx, move |_| async move {
168			let result = task.await.map_err(|err| error_callback(action, &path, &err, error_data));
169			callback_after();
170			result
171		})
172	}
173}
174
175#[js_class]
176impl FileHandle {
177	pub fn read<'cx>(&self, cx: &'cx Context, Opt(array): Opt<Uint8Array>) -> Option<Promise<'cx>> {
178		let path = Arc::clone(&self.path);
179		let bytes = array.as_ref().map(|array| unsafe {
180			let (ptr, len) = array.data();
181			slice::from_raw_parts_mut(ptr, len)
182		});
183		let array = array.as_ref().map(|array| TracedHeap::new(array.handle().get()));
184
185		self.with_blocking_promise(
186			cx,
187			"read",
188			path,
189			move |file| read_inner(file, bytes),
190			|| drop(array),
191			file_error,
192			(),
193		)
194	}
195
196	#[ion(name = "readSync")]
197	pub fn read_sync(&self, Opt(array): Opt<Uint8Array>) -> Result<ReadResult> {
198		self.with_sync(|file| {
199			let bytes = array.as_ref().map(|array| unsafe { array.as_mut_slice() });
200			read_inner(file, bytes).map_err(|err| file_error("read", &self.path, &err, ()))
201		})
202	}
203
204	pub fn write<'cx>(
205		&self, cx: &'cx Context, #[ion(convert = false)] contents: BufferSource<'cx>,
206	) -> Option<Promise<'cx>> {
207		let path = Arc::clone(&self.path);
208		let contents = contents.to_vec();
209		self.with_blocking_promise(
210			cx,
211			"write",
212			path,
213			move |file| file.write(&contents).map(|bytes| bytes as u64),
214			|| {},
215			file_error,
216			(),
217		)
218	}
219
220	#[ion(name = "writeSync")]
221	pub fn write_sync(&self, #[ion(convert = false)] contents: BufferSource) -> Result<u64> {
222		self.with_sync(|file| {
223			let contents = unsafe { contents.as_slice() };
224			match file.write(contents) {
225				Ok(bytes) => Ok(bytes as u64),
226				Err(err) => Err(file_error("write", &self.path, &err, ())),
227			}
228		})
229	}
230
231	#[ion(name = "writeAll")]
232	pub fn write_all<'cx>(
233		&self, cx: &'cx Context, #[ion(convert = false)] contents: BufferSource<'cx>,
234	) -> Option<Promise<'cx>> {
235		let path = Arc::clone(&self.path);
236		let contents = contents.to_vec();
237		self.with_blocking_promise(
238			cx,
239			"write",
240			path,
241			move |file| file.write_all(&contents).map(|_| ()),
242			|| {},
243			file_error,
244			(),
245		)
246	}
247
248	#[ion(name = "writeAllSync")]
249	pub fn write_all_sync(&self, #[ion(convert = false)] contents: BufferSource) -> Result<()> {
250		self.with_sync(|file| {
251			let contents = unsafe { contents.as_slice() };
252			match file.write_all(contents) {
253				Ok(_) => Ok(()),
254				Err(err) => Err(file_error("write", &self.path, &err, ())),
255			}
256		})
257	}
258
259	pub fn truncate<'cx>(
260		&self, cx: &'cx Context, #[ion(convert = ConversionBehavior::EnforceRange
261		)]
262		Opt(length): Opt<u64>,
263	) -> Option<Promise<'cx>> {
264		let path = Arc::clone(&self.path);
265		self.with_blocking_promise(
266			cx,
267			"truncate",
268			path,
269			move |file| file.set_len(length.unwrap_or(0)),
270			|| {},
271			file_error,
272			(),
273		)
274	}
275
276	#[ion(name = "truncateSync")]
277	pub fn truncate_sync(
278		&self, #[ion(convert = ConversionBehavior::EnforceRange)] Opt(length): Opt<u64>,
279	) -> Result<()> {
280		self.with_sync(|file| {
281			file.set_len(length.unwrap_or(0))
282				.map_err(|err| file_error("truncate", &self.path, &err, ()))
283		})
284	}
285
286	pub fn seek<'cx>(
287		&self, cx: &'cx Context, #[ion(convert = ConversionBehavior::EnforceRange)] offset: i64,
288		Opt(mode): Opt<SeekMode>,
289	) -> Option<Promise<'cx>> {
290		let path = Arc::clone(&self.path);
291		let mode = mode.unwrap_or_default();
292		self.with_blocking_promise(
293			cx,
294			"seek",
295			path,
296			move |file| {
297				let seek = seek_from_mode(offset, mode);
298				file.seek(seek)
299			},
300			|| {},
301			seek_error,
302			(mode, offset),
303		)
304	}
305
306	#[ion(name = "seekSync")]
307	pub fn seek_sync(
308		&self, #[ion(convert = ConversionBehavior::EnforceRange
309		)]
310		offset: i64, Opt(mode): Opt<SeekMode>,
311	) -> Result<u64> {
312		self.with_sync(|file| {
313			let mode = mode.unwrap_or_default();
314			let seek = seek_from_mode(offset, mode);
315			file.seek(seek).map_err(|err| seek_error("", &self.path, &err, (mode, offset)))
316		})
317	}
318
319	pub fn sync<'cx>(&self, cx: &'cx Context) -> Option<Promise<'cx>> {
320		let path = Arc::clone(&self.path);
321		self.with_blocking_promise(cx, "sync", path, move |file| file.sync_all(), || {}, file_error, ())
322	}
323
324	#[ion(name = "syncSync")]
325	pub fn sync_sync(&self) -> Result<()> {
326		self.with_sync(|file| file.sync_all().map_err(|err| file_error("sync", &self.path, &err, ())))
327	}
328
329	#[ion(name = "syncData")]
330	pub fn sync_data<'cx>(&self, cx: &'cx Context) -> Option<Promise<'cx>> {
331		let path = Arc::clone(&self.path);
332		self.with_blocking_promise(
333			cx,
334			"sync data for",
335			path,
336			move |file| file.sync_data(),
337			|| {},
338			file_error,
339			(),
340		)
341	}
342
343	#[ion(name = "syncDataSync")]
344	pub fn sync_data_sync(&self) -> Result<()> {
345		self.with_sync(|file| file.sync_data().map_err(|err| file_error("sync data for", &self.path, &err, ())))
346	}
347
348	pub fn metadata<'cx>(&self, cx: &'cx Context) -> Option<Promise<'cx>> {
349		let path = Arc::clone(&self.path);
350		self.with_blocking_promise(
351			cx,
352			"get metadata for",
353			path,
354			move |file| file.metadata().map(|meta| Metadata::new(&meta)),
355			|| {},
356			file_error,
357			(),
358		)
359	}
360
361	#[ion(name = "metadataSync")]
362	pub fn metadata_sync(&self) -> Result<Metadata> {
363		self.with_sync(|file| {
364			file.metadata()
365				.as_ref()
366				.map(Metadata::new)
367				.map_err(|err| file_error("get metadata for", &self.path, err, ()))
368		})
369	}
370}
371
372fn read_inner(file: &mut File, bytes: Option<&mut [u8]>) -> io::Result<ReadResult> {
373	if let Some(bytes) = bytes {
374		file.read(bytes).map(ReadResult::BytesWritten)
375	} else {
376		let size = file.metadata().map(|m| m.len() as usize).ok();
377		let mut bytes = Vec::new();
378		bytes.reserve_exact(size.unwrap_or(0));
379		file.read_to_end(&mut bytes).map(|_| ReadResult::Buffer(bytes))
380	}
381}
382
383fn seek_from_mode(offset: i64, mode: SeekMode) -> SeekFrom {
384	use SeekMode as SM;
385	match mode {
386		SM::Current => SeekFrom::Current(offset),
387		SM::Start => SeekFrom::Start(u64::try_from(offset.max(0)).unwrap()),
388		SM::End => SeekFrom::End(offset),
389	}
390}