script/dom/
filereader.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::ptr;
7use std::rc::Rc;
8
9use base64::Engine;
10use dom_struct::dom_struct;
11use encoding_rs::{Encoding, UTF_8};
12use js::jsapi::{Heap, JSObject};
13use js::jsval::{self, JSVal};
14use js::rust::HandleObject;
15use js::typedarray::{ArrayBuffer, CreateWith};
16use mime::{self, Mime};
17use script_bindings::num::Finite;
18use stylo_atoms::Atom;
19
20use crate::dom::bindings::cell::DomRefCell;
21use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
22use crate::dom::bindings::codegen::Bindings::FileReaderBinding::{
23    FileReaderConstants, FileReaderMethods,
24};
25use crate::dom::bindings::codegen::UnionTypes::StringOrObject;
26use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
30use crate::dom::bindings::root::{DomRoot, MutNullableDom};
31use crate::dom::bindings::str::DOMString;
32use crate::dom::bindings::trace::RootedTraceableBox;
33use crate::dom::blob::Blob;
34use crate::dom::domexception::{DOMErrorName, DOMException};
35use crate::dom::event::{Event, EventBubbles, EventCancelable};
36use crate::dom::eventtarget::EventTarget;
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::progressevent::ProgressEvent;
39use crate::realms::enter_realm;
40use crate::script_runtime::{CanGc, JSContext};
41use crate::task::TaskOnce;
42
43pub(crate) enum FileReadingTask {
44    ProcessRead(TrustedFileReader, GenerationId),
45    ProcessReadData(TrustedFileReader, GenerationId),
46    ProcessReadError(TrustedFileReader, GenerationId, DOMErrorName),
47    ProcessReadEOF(TrustedFileReader, GenerationId, ReadMetaData, Vec<u8>),
48}
49
50impl TaskOnce for FileReadingTask {
51    fn run_once(self) {
52        self.handle_task(CanGc::note());
53    }
54}
55
56impl FileReadingTask {
57    pub(crate) fn handle_task(self, can_gc: CanGc) {
58        use self::FileReadingTask::*;
59
60        match self {
61            ProcessRead(reader, gen_id) => FileReader::process_read(reader, gen_id, can_gc),
62            ProcessReadData(reader, gen_id) => {
63                FileReader::process_read_data(reader, gen_id, can_gc)
64            },
65            ProcessReadError(reader, gen_id, error) => {
66                FileReader::process_read_error(reader, gen_id, error, can_gc)
67            },
68            ProcessReadEOF(reader, gen_id, metadata, blob_contents) => {
69                FileReader::process_read_eof(reader, gen_id, metadata, blob_contents, can_gc)
70            },
71        }
72    }
73}
74#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
75pub(crate) enum FileReaderFunction {
76    Text,
77    DataUrl,
78    ArrayBuffer,
79}
80
81pub(crate) type TrustedFileReader = Trusted<FileReader>;
82
83#[derive(Clone, MallocSizeOf)]
84pub(crate) struct ReadMetaData {
85    pub(crate) blobtype: String,
86    pub(crate) label: Option<String>,
87    pub(crate) function: FileReaderFunction,
88}
89
90impl ReadMetaData {
91    pub(crate) fn new(
92        blobtype: String,
93        label: Option<String>,
94        function: FileReaderFunction,
95    ) -> ReadMetaData {
96        ReadMetaData {
97            blobtype,
98            label,
99            function,
100        }
101    }
102}
103
104#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
105pub(crate) struct GenerationId(u32);
106
107#[repr(u16)]
108#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
109pub(crate) enum FileReaderReadyState {
110    Empty = FileReaderConstants::EMPTY,
111    Loading = FileReaderConstants::LOADING,
112    Done = FileReaderConstants::DONE,
113}
114
115#[derive(JSTraceable, MallocSizeOf)]
116pub(crate) enum FileReaderResult {
117    ArrayBuffer(#[ignore_malloc_size_of = "mozjs"] RootedTraceableBox<Heap<JSVal>>),
118    String(DOMString),
119}
120
121pub(crate) struct FileReaderSharedFunctionality;
122
123impl FileReaderSharedFunctionality {
124    pub(crate) fn dataurl_format(blob_contents: &[u8], blob_type: String) -> DOMString {
125        let base64 = base64::engine::general_purpose::STANDARD.encode(blob_contents);
126
127        let dataurl = if blob_type.is_empty() {
128            format!("data:base64,{}", base64)
129        } else {
130            format!("data:{};base64,{}", blob_type, base64)
131        };
132
133        DOMString::from(dataurl)
134    }
135
136    pub(crate) fn text_decode(
137        blob_contents: &[u8],
138        blob_type: &str,
139        blob_label: &Option<String>,
140    ) -> DOMString {
141        // https://w3c.github.io/FileAPI/#encoding-determination
142        // Steps 1 & 2 & 3
143        let mut encoding = blob_label
144            .as_ref()
145            .map(|string| string.as_bytes())
146            .and_then(Encoding::for_label);
147
148        // Step 4 & 5
149        encoding = encoding.or_else(|| {
150            let resultmime = blob_type.parse::<Mime>().ok();
151            resultmime.and_then(|mime| {
152                mime.params()
153                    .find(|(k, _)| &mime::CHARSET == k)
154                    .and_then(|(_, v)| Encoding::for_label(v.as_ref().as_bytes()))
155            })
156        });
157
158        // Step 6
159        let enc = encoding.unwrap_or(UTF_8);
160
161        let convert = blob_contents;
162        // Step 7
163        let (output, _, _) = enc.decode(convert);
164        DOMString::from(output)
165    }
166}
167
168#[dom_struct]
169pub(crate) struct FileReader {
170    eventtarget: EventTarget,
171    ready_state: Cell<FileReaderReadyState>,
172    error: MutNullableDom<DOMException>,
173    result: DomRefCell<Option<FileReaderResult>>,
174    generation_id: Cell<GenerationId>,
175}
176
177impl FileReader {
178    pub(crate) fn new_inherited() -> FileReader {
179        FileReader {
180            eventtarget: EventTarget::new_inherited(),
181            ready_state: Cell::new(FileReaderReadyState::Empty),
182            error: MutNullableDom::new(None),
183            result: DomRefCell::new(None),
184            generation_id: Cell::new(GenerationId(0)),
185        }
186    }
187
188    fn new(
189        global: &GlobalScope,
190        proto: Option<HandleObject>,
191        can_gc: CanGc,
192    ) -> DomRoot<FileReader> {
193        reflect_dom_object_with_proto(Box::new(FileReader::new_inherited()), global, proto, can_gc)
194    }
195
196    // https://w3c.github.io/FileAPI/#dfn-error-steps
197    pub(crate) fn process_read_error(
198        filereader: TrustedFileReader,
199        gen_id: GenerationId,
200        error: DOMErrorName,
201        can_gc: CanGc,
202    ) {
203        let fr = filereader.root();
204
205        macro_rules! return_on_abort(
206            () => (
207                if gen_id != fr.generation_id.get() {
208                    return
209                }
210            );
211        );
212
213        return_on_abort!();
214        // Step 1
215        fr.change_ready_state(FileReaderReadyState::Done);
216        *fr.result.borrow_mut() = None;
217
218        let exception = DOMException::new(&fr.global(), error, can_gc);
219        fr.error.set(Some(&exception));
220
221        fr.dispatch_progress_event(atom!("error"), 0, None, can_gc);
222        return_on_abort!();
223        // Step 3
224        fr.dispatch_progress_event(atom!("loadend"), 0, None, can_gc);
225        return_on_abort!();
226        // Step 4
227        fr.terminate_ongoing_reading();
228    }
229
230    // https://w3c.github.io/FileAPI/#dfn-readAsText
231    pub(crate) fn process_read_data(
232        filereader: TrustedFileReader,
233        gen_id: GenerationId,
234        can_gc: CanGc,
235    ) {
236        let fr = filereader.root();
237
238        macro_rules! return_on_abort(
239            () => (
240                if gen_id != fr.generation_id.get() {
241                    return
242                }
243            );
244        );
245        return_on_abort!();
246        // FIXME Step 7 send current progress
247        fr.dispatch_progress_event(atom!("progress"), 0, None, can_gc);
248    }
249
250    // https://w3c.github.io/FileAPI/#dfn-readAsText
251    pub(crate) fn process_read(filereader: TrustedFileReader, gen_id: GenerationId, can_gc: CanGc) {
252        let fr = filereader.root();
253
254        macro_rules! return_on_abort(
255            () => (
256                if gen_id != fr.generation_id.get() {
257                    return
258                }
259            );
260        );
261        return_on_abort!();
262        // Step 6
263        fr.dispatch_progress_event(atom!("loadstart"), 0, None, can_gc);
264    }
265
266    // https://w3c.github.io/FileAPI/#dfn-readAsText
267    pub(crate) fn process_read_eof(
268        filereader: TrustedFileReader,
269        gen_id: GenerationId,
270        data: ReadMetaData,
271        blob_contents: Vec<u8>,
272        can_gc: CanGc,
273    ) {
274        let fr = filereader.root();
275
276        macro_rules! return_on_abort(
277            () => (
278                if gen_id != fr.generation_id.get() {
279                    return
280                }
281            );
282        );
283
284        return_on_abort!();
285        // Step 8.1
286        fr.change_ready_state(FileReaderReadyState::Done);
287        // Step 8.2
288
289        match data.function {
290            FileReaderFunction::DataUrl => {
291                FileReader::perform_readasdataurl(&fr.result, data, &blob_contents)
292            },
293            FileReaderFunction::Text => {
294                FileReader::perform_readastext(&fr.result, data, &blob_contents)
295            },
296            FileReaderFunction::ArrayBuffer => {
297                let _ac = enter_realm(&*fr);
298                FileReader::perform_readasarraybuffer(
299                    &fr.result,
300                    GlobalScope::get_cx(),
301                    data,
302                    &blob_contents,
303                )
304            },
305        };
306
307        // Step 8.3
308        fr.dispatch_progress_event(atom!("load"), 0, None, can_gc);
309        return_on_abort!();
310        // Step 8.4
311        if fr.ready_state.get() != FileReaderReadyState::Loading {
312            fr.dispatch_progress_event(atom!("loadend"), 0, None, can_gc);
313        }
314        return_on_abort!();
315    }
316
317    /// <https://w3c.github.io/FileAPI/#dfn-readAsText>
318    fn perform_readastext(
319        result: &DomRefCell<Option<FileReaderResult>>,
320        data: ReadMetaData,
321        blob_bytes: &[u8],
322    ) {
323        let blob_label = &data.label;
324        let blob_type = &data.blobtype;
325
326        let output = FileReaderSharedFunctionality::text_decode(blob_bytes, blob_type, blob_label);
327        *result.borrow_mut() = Some(FileReaderResult::String(output));
328    }
329
330    /// <https://w3c.github.io/FileAPI/#dfn-readAsDataURL>
331    fn perform_readasdataurl(
332        result: &DomRefCell<Option<FileReaderResult>>,
333        data: ReadMetaData,
334        bytes: &[u8],
335    ) {
336        let output = FileReaderSharedFunctionality::dataurl_format(bytes, data.blobtype);
337
338        *result.borrow_mut() = Some(FileReaderResult::String(output));
339    }
340
341    // https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
342    #[expect(unsafe_code)]
343    fn perform_readasarraybuffer(
344        result: &DomRefCell<Option<FileReaderResult>>,
345        cx: JSContext,
346        _: ReadMetaData,
347        bytes: &[u8],
348    ) {
349        unsafe {
350            rooted!(in(*cx) let mut array_buffer = ptr::null_mut::<JSObject>());
351            assert!(
352                ArrayBuffer::create(*cx, CreateWith::Slice(bytes), array_buffer.handle_mut())
353                    .is_ok()
354            );
355
356            *result.borrow_mut() =
357                Some(FileReaderResult::ArrayBuffer(RootedTraceableBox::default()));
358
359            if let Some(FileReaderResult::ArrayBuffer(ref mut heap)) = *result.borrow_mut() {
360                heap.set(jsval::ObjectValue(array_buffer.get()));
361            };
362        }
363    }
364}
365
366impl FileReaderMethods<crate::DomTypeHolder> for FileReader {
367    /// <https://w3c.github.io/FileAPI/#filereaderConstrctr>
368    fn Constructor(
369        global: &GlobalScope,
370        proto: Option<HandleObject>,
371        can_gc: CanGc,
372    ) -> Fallible<DomRoot<FileReader>> {
373        Ok(FileReader::new(global, proto, can_gc))
374    }
375
376    // https://w3c.github.io/FileAPI/#dfn-onloadstart
377    event_handler!(loadstart, GetOnloadstart, SetOnloadstart);
378
379    // https://w3c.github.io/FileAPI/#dfn-onprogress
380    event_handler!(progress, GetOnprogress, SetOnprogress);
381
382    // https://w3c.github.io/FileAPI/#dfn-onload
383    event_handler!(load, GetOnload, SetOnload);
384
385    // https://w3c.github.io/FileAPI/#dfn-onabort
386    event_handler!(abort, GetOnabort, SetOnabort);
387
388    // https://w3c.github.io/FileAPI/#dfn-onerror
389    event_handler!(error, GetOnerror, SetOnerror);
390
391    // https://w3c.github.io/FileAPI/#dfn-onloadend
392    event_handler!(loadend, GetOnloadend, SetOnloadend);
393
394    // https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
395    fn ReadAsArrayBuffer(&self, blob: &Blob, can_gc: CanGc) -> ErrorResult {
396        self.read(FileReaderFunction::ArrayBuffer, blob, None, can_gc)
397    }
398
399    // https://w3c.github.io/FileAPI/#dfn-readAsDataURL
400    fn ReadAsDataURL(&self, blob: &Blob, can_gc: CanGc) -> ErrorResult {
401        self.read(FileReaderFunction::DataUrl, blob, None, can_gc)
402    }
403
404    // https://w3c.github.io/FileAPI/#dfn-readAsText
405    fn ReadAsText(&self, blob: &Blob, label: Option<DOMString>, can_gc: CanGc) -> ErrorResult {
406        self.read(FileReaderFunction::Text, blob, label, can_gc)
407    }
408
409    /// <https://w3c.github.io/FileAPI/#dfn-abort>
410    fn Abort(&self, can_gc: CanGc) {
411        // Step 2
412        if self.ready_state.get() == FileReaderReadyState::Loading {
413            self.change_ready_state(FileReaderReadyState::Done);
414        }
415        // Steps 1 & 3
416        *self.result.borrow_mut() = None;
417
418        let exception = DOMException::new(&self.global(), DOMErrorName::AbortError, can_gc);
419        self.error.set(Some(&exception));
420
421        self.terminate_ongoing_reading();
422        // Steps 5 & 6
423        self.dispatch_progress_event(atom!("abort"), 0, None, can_gc);
424        self.dispatch_progress_event(atom!("loadend"), 0, None, can_gc);
425    }
426
427    /// <https://w3c.github.io/FileAPI/#dfn-error>
428    fn GetError(&self) -> Option<DomRoot<DOMException>> {
429        self.error.get()
430    }
431
432    #[expect(unsafe_code)]
433    /// <https://w3c.github.io/FileAPI/#dfn-result>
434    fn GetResult(&self, _: JSContext) -> Option<StringOrObject> {
435        self.result.borrow().as_ref().map(|r| match *r {
436            FileReaderResult::String(ref string) => StringOrObject::String(string.clone()),
437            FileReaderResult::ArrayBuffer(ref arr_buffer) => {
438                let result = RootedTraceableBox::new(Heap::default());
439                unsafe {
440                    result.set((*arr_buffer.ptr.get()).to_object());
441                }
442                StringOrObject::Object(result)
443            },
444        })
445    }
446
447    /// <https://w3c.github.io/FileAPI/#dfn-readyState>
448    fn ReadyState(&self) -> u16 {
449        self.ready_state.get() as u16
450    }
451}
452
453impl FileReader {
454    fn dispatch_progress_event(&self, type_: Atom, loaded: u64, total: Option<u64>, can_gc: CanGc) {
455        let progressevent = ProgressEvent::new(
456            &self.global(),
457            type_,
458            EventBubbles::DoesNotBubble,
459            EventCancelable::NotCancelable,
460            total.is_some(),
461            Finite::wrap(loaded as f64),
462            Finite::wrap(total.unwrap_or(0) as f64),
463            can_gc,
464        );
465        progressevent.upcast::<Event>().fire(self.upcast(), can_gc);
466    }
467
468    fn terminate_ongoing_reading(&self) {
469        let GenerationId(prev_id) = self.generation_id.get();
470        self.generation_id.set(GenerationId(prev_id + 1));
471    }
472
473    /// <https://w3c.github.io/FileAPI/#readOperation>
474    fn read(
475        &self,
476        function: FileReaderFunction,
477        blob: &Blob,
478        label: Option<DOMString>,
479        can_gc: CanGc,
480    ) -> ErrorResult {
481        let cx = GlobalScope::get_cx();
482
483        // If fr’s state is "loading", throw an InvalidStateError DOMException.
484        if self.ready_state.get() == FileReaderReadyState::Loading {
485            return Err(Error::InvalidState(None));
486        }
487
488        // Set fr’s state to "loading".
489        self.change_ready_state(FileReaderReadyState::Loading);
490
491        // Set fr’s result to null.
492        *self.result.borrow_mut() = None;
493
494        // Set fr’s error to null.
495        // See the note below in the error steps.
496
497        // Let stream be the result of calling get stream on blob.
498        let stream = blob.get_stream(can_gc);
499
500        // Let reader be the result of getting a reader from stream.
501        let reader = stream.and_then(|s| s.acquire_default_reader(can_gc))?;
502
503        let type_ = blob.Type();
504
505        let load_data = ReadMetaData::new(String::from(type_), label.map(String::from), function);
506
507        let GenerationId(prev_id) = self.generation_id.get();
508        self.generation_id.set(GenerationId(prev_id + 1));
509        let gen_id = self.generation_id.get();
510
511        let filereader_success = DomRoot::from_ref(self);
512        let filereader_error = DomRoot::from_ref(self);
513
514        // In parallel, while true:
515        // Wait for chunkPromise to be fulfilled or rejected.
516        // Note: the spec appears wrong or outdated,
517        // so for now we use the simple `read_all_bytes` call,
518        // which means we cannot fire the progress event at each chunk.
519        // This can be revisisted following the discussion at
520        // <https://github.com/w3c/FileAPI/issues/208>
521
522        // Read all bytes from stream with reader.
523        reader.read_all_bytes(
524            cx,
525            Rc::new(move |blob_contents| {
526                let global = filereader_success.global();
527                let task_manager = global.task_manager();
528                let task_source = task_manager.file_reading_task_source();
529
530                // If chunkPromise is fulfilled,
531                // and isFirstChunk is true,
532                // queue a task
533                // Note: this should be done for the first chunk,
534                // see issue above.
535                task_source.queue(FileReadingTask::ProcessRead(
536                    Trusted::new(&filereader_success.clone()),
537                    gen_id,
538                ));
539                // If chunkPromise is fulfilled
540                // with an object whose done property is false
541                // and whose value property is a Uint8Array object
542                // Note: this should be done for each chunk,
543                // see issue above.
544                if !blob_contents.is_empty() {
545                    task_source.queue(FileReadingTask::ProcessReadData(
546                        Trusted::new(&filereader_success.clone()),
547                        gen_id,
548                    ));
549                }
550                // Otherwise,
551                // if chunkPromise is fulfilled with an object whose done property is true,
552                // queue a task
553                // Note: we are in the succes steps of `read_all_bytes`,
554                // so the last chunk has been received.
555                task_source.queue(FileReadingTask::ProcessReadEOF(
556                    Trusted::new(&filereader_success.clone()),
557                    gen_id,
558                    load_data.clone(),
559                    blob_contents.to_vec(),
560                ));
561            }),
562            Rc::new(move |_cx, _error| {
563                let global = filereader_error.global();
564                let task_manager = global.task_manager();
565                let task_source = task_manager.file_reading_task_source();
566
567                // Otherwise, if chunkPromise is rejected with an error error,
568                // queue a task
569                // Note: not using the error from `read_all_bytes`,
570                // see issue above.
571                task_source.queue(FileReadingTask::ProcessReadError(
572                    Trusted::new(&filereader_error),
573                    gen_id,
574                    DOMErrorName::OperationError,
575                ));
576            }),
577            can_gc,
578        );
579        Ok(())
580    }
581
582    fn change_ready_state(&self, state: FileReaderReadyState) {
583        self.ready_state.set(state);
584    }
585}