runtime/globals/file/
reader.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::UnsafeCell;
8use std::str::FromStr as _;
9
10use base64::Engine as _;
11use base64::prelude::BASE64_STANDARD;
12use encoding_rs::{Encoding, UTF_8};
13use ion::class::{NativeObject as _, Reflector};
14use ion::conversions::ToValue as _;
15use ion::function::Opt;
16use ion::string::byte::{ByteString, Latin1};
17use ion::typedarray::ArrayBufferWrapper;
18use ion::{ClassDefinition as _, Context, Error, ErrorKind, Object, Result, Traceable, TracedHeap, js_class};
19use mime::Mime;
20use mozjs::jsapi::{Heap, JSObject};
21use mozjs::jsval::{JSVal, NullValue};
22
23use crate::globals::file::Blob;
24use crate::promise::future_to_promise;
25
26fn encoding_from_string_mime(encoding: Option<&str>, mime: Option<&str>) -> &'static Encoding {
27	encoding
28		.and_then(|e| match Encoding::for_label_no_replacement(e.as_bytes()) {
29			None if mime.is_some() => Mime::from_str(mime.unwrap()).ok().and_then(|mime| {
30				Encoding::for_label_no_replacement(mime.get_param("charset").map_or(b"", |p| p.as_str().as_bytes()))
31			}),
32			e => e,
33		})
34		.unwrap_or(UTF_8)
35}
36
37#[derive(Debug, Default, Copy, Clone, PartialEq, Traceable)]
38#[repr(u8)]
39pub enum FileReaderState {
40	#[default]
41	Empty = 0,
42	Loading = 1,
43	Done = 2,
44}
45
46impl FileReaderState {
47	fn validate(&mut self) -> Result<()> {
48		if FileReaderState::Loading == *self {
49			return Err(Error::new("Invalid State for File Reader", ErrorKind::Type));
50		}
51		*self = FileReaderState::Loading;
52		Ok(())
53	}
54}
55
56#[derive(Debug)]
57#[js_class]
58pub struct FileReader {
59	reflector: Reflector,
60	state: FileReaderState,
61	result: Heap<JSVal>,
62	error: Heap<*mut JSObject>,
63}
64
65#[js_class]
66impl FileReader {
67	pub const EMPTY: i32 = FileReaderState::Empty as u8 as i32;
68	pub const LOADING: i32 = FileReaderState::Loading as u8 as i32;
69	pub const DONE: i32 = FileReaderState::Done as u8 as i32;
70
71	#[ion(constructor)]
72	pub fn constructor() -> FileReader {
73		FileReader::default()
74	}
75
76	#[ion(get)]
77	pub fn get_ready_state(&self) -> u8 {
78		self.state as u8
79	}
80
81	#[ion(get)]
82	pub fn get_result(&self) -> JSVal {
83		self.result.get()
84	}
85
86	#[ion(get)]
87	pub fn get_error(&self) -> *mut JSObject {
88		self.error.get()
89	}
90
91	#[ion(name = "readAsArrayBuffer")]
92	pub fn read_as_array_buffer(&mut self, cx: &Context, blob: &Blob) -> Result<()> {
93		self.state.validate()?;
94		let bytes = blob.bytes.clone();
95
96		let this = TracedHeap::new(self.reflector().get());
97
98		future_to_promise::<_, _, _, Error>(cx, |cx| async move {
99			let reader = Object::from(this.to_local());
100			let reader = FileReader::get_private(&cx, &reader)?;
101			let array_buffer = ArrayBufferWrapper::from(bytes.to_vec());
102			reader.result.set(array_buffer.as_value(&cx).get());
103			Ok(())
104		});
105		Ok(())
106	}
107
108	#[ion(name = "readAsBinaryString")]
109	pub fn read_as_binary_string(&mut self, cx: &Context, blob: &Blob) -> Result<()> {
110		self.state.validate()?;
111		let bytes = blob.bytes.clone();
112
113		let this = TracedHeap::new(self.reflector().get());
114
115		future_to_promise::<_, _, _, Error>(cx, |cx| async move {
116			let reader = Object::from(this.to_local());
117			let reader = FileReader::get_private(&cx, &reader)?;
118			let byte_string = unsafe { ByteString::<Latin1>::from_unchecked(bytes.to_vec()) };
119			reader.result.set(byte_string.as_value(&cx).get());
120			Ok(())
121		});
122		Ok(())
123	}
124
125	#[ion(name = "readAsText")]
126	pub fn read_as_text(&mut self, cx: &Context, blob: &Blob, Opt(encoding): Opt<String>) -> Result<()> {
127		self.state.validate()?;
128		let bytes = blob.bytes.clone();
129		let mime = blob.kind.clone();
130
131		let this = TracedHeap::new(self.reflector().get());
132
133		future_to_promise::<_, _, _, Error>(cx, |cx| async move {
134			let encoding = encoding_from_string_mime(encoding.as_deref(), mime.as_deref());
135
136			let reader = Object::from(this.to_local());
137			let reader = FileReader::get_private(&cx, &reader)?;
138			let str = encoding.decode_without_bom_handling(&bytes).0;
139			reader.result.set(str.as_value(&cx).get());
140			Ok(())
141		});
142		Ok(())
143	}
144
145	#[ion(name = "readAsDataURL")]
146	pub fn read_as_data_url(&mut self, cx: &Context, blob: &Blob) -> Result<()> {
147		self.state.validate()?;
148		let bytes = blob.bytes.clone();
149		let mime = blob.kind.clone();
150
151		let this = TracedHeap::new(self.reflector().get());
152
153		future_to_promise::<_, _, _, Error>(cx, |cx| async move {
154			let reader = Object::from(this.to_local());
155			let reader = FileReader::get_private(&cx, &reader)?;
156			let base64 = BASE64_STANDARD.encode(&bytes);
157			let data_url = match mime {
158				Some(mime) => format!("data:{mime};base64,{base64}"),
159				None => format!("data:base64,{base64}"),
160			};
161
162			reader.result.set(data_url.as_value(&cx).get());
163			Ok(())
164		});
165		Ok(())
166	}
167}
168
169impl Default for FileReader {
170	fn default() -> FileReader {
171		FileReader {
172			reflector: Reflector::default(),
173			state: FileReaderState::default(),
174			result: Heap { ptr: UnsafeCell::from(NullValue()) },
175			error: Heap::default(),
176		}
177	}
178}
179
180#[derive(Debug, Default)]
181#[js_class]
182pub struct FileReaderSync {
183	reflector: Reflector,
184}
185
186#[js_class]
187impl FileReaderSync {
188	#[ion(constructor)]
189	pub fn constructor() -> FileReaderSync {
190		FileReaderSync::default()
191	}
192
193	#[ion(name = "readAsArrayBuffer")]
194	#[expect(clippy::unused_self)]
195	pub fn read_as_array_buffer(&mut self, blob: &Blob) -> ArrayBufferWrapper {
196		ArrayBufferWrapper::from(blob.bytes.to_vec())
197	}
198
199	#[ion(name = "readAsBinaryString")]
200	#[expect(clippy::unused_self)]
201	pub fn read_as_binary_string(&mut self, blob: &Blob) -> ByteString {
202		unsafe { ByteString::<Latin1>::from_unchecked(blob.bytes.to_vec()) }
203	}
204
205	#[ion(name = "readAsText")]
206	#[expect(clippy::unused_self)]
207	pub fn read_as_text(&mut self, blob: &Blob, Opt(encoding): Opt<String>) -> String {
208		let encoding = encoding_from_string_mime(encoding.as_deref(), blob.kind.as_deref());
209		encoding.decode_without_bom_handling(&blob.bytes).0.into_owned()
210	}
211
212	#[ion(name = "readAsDataURL")]
213	#[expect(clippy::unused_self)]
214	pub fn read_as_data_url(&mut self, blob: &Blob) -> String {
215		let mime = blob.kind.clone();
216
217		let base64 = BASE64_STANDARD.encode(&blob.bytes);
218		match mime {
219			Some(mime) => format!("data:{mime};base64,{base64}"),
220			None => format!("data:base64,{base64}"),
221		}
222	}
223}