Skip to main content

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