runtime/globals/file/
reader.rs1use 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}