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(cx, CreateWith::Slice(bytes), array_buffer.handle_mut())
388                    .is_ok()
389            );
390
391            *result.borrow_mut() =
392                Some(FileReaderResult::ArrayBuffer(RootedTraceableBox::default()));
393
394            if let Some(FileReaderResult::ArrayBuffer(ref mut heap)) = *result.borrow_mut() {
395                heap.set(jsval::ObjectValue(array_buffer.get()));
396            };
397        }
398    }
399}
400
401impl FileReaderMethods<crate::DomTypeHolder> for FileReader {
402    /// <https://w3c.github.io/FileAPI/#filereaderConstrctr>
403    fn Constructor(
404        cx: &mut js::context::JSContext,
405        global: &GlobalScope,
406        proto: Option<HandleObject>,
407    ) -> Fallible<DomRoot<FileReader>> {
408        Ok(FileReader::new(cx, global, proto))
409    }
410
411    // https://w3c.github.io/FileAPI/#dfn-onloadstart
412    event_handler!(loadstart, GetOnloadstart, SetOnloadstart);
413
414    // https://w3c.github.io/FileAPI/#dfn-onprogress
415    event_handler!(progress, GetOnprogress, SetOnprogress);
416
417    // https://w3c.github.io/FileAPI/#dfn-onload
418    event_handler!(load, GetOnload, SetOnload);
419
420    // https://w3c.github.io/FileAPI/#dfn-onabort
421    event_handler!(abort, GetOnabort, SetOnabort);
422
423    // https://w3c.github.io/FileAPI/#dfn-onerror
424    event_handler!(error, GetOnerror, SetOnerror);
425
426    // https://w3c.github.io/FileAPI/#dfn-onloadend
427    event_handler!(loadend, GetOnloadend, SetOnloadend);
428
429    /// <https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer>
430    fn ReadAsArrayBuffer(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
431        // > The readAsArrayBuffer(blob) method, when invoked,
432        // must initiate a read operation for blob with ArrayBuffer.
433        self.read(cx, FileReaderFunction::ArrayBuffer, blob, None)
434    }
435
436    /// <https://w3c.github.io/FileAPI/#dfn-readAsBinaryString>
437    fn ReadAsBinaryString(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
438        // > The readAsBinaryString(blob) method, when invoked,
439        // must initiate a read operation for blob with BinaryString.
440        self.read(cx, FileReaderFunction::BinaryString, blob, None)
441    }
442
443    /// <https://w3c.github.io/FileAPI/#dfn-readAsDataURL>
444    fn ReadAsDataURL(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
445        // > The readAsDataURL(blob) method, when invoked,
446        // must initiate a read operation for blob with DataURL.
447        self.read(cx, FileReaderFunction::DataUrl, blob, None)
448    }
449
450    /// <https://w3c.github.io/FileAPI/#dfn-readAsText>
451    fn ReadAsText(
452        &self,
453        cx: &mut js::context::JSContext,
454        blob: &Blob,
455        encoding: Option<DOMString>,
456    ) -> ErrorResult {
457        // > The readAsText(blob, encoding) method, when invoked,
458        // must initiate a read operation for blob with Text and encoding.
459        self.read(cx, FileReaderFunction::Text, blob, encoding)
460    }
461
462    /// <https://w3c.github.io/FileAPI/#dfn-abort>
463    fn Abort(&self, cx: &mut js::context::JSContext) {
464        // Step 2
465        if self.ready_state.get() == FileReaderReadyState::Loading {
466            self.change_ready_state(FileReaderReadyState::Done);
467        }
468        // Steps 1 & 3
469        *self.result.borrow_mut() = None;
470
471        let exception = DOMException::new(cx, &self.global(), DOMErrorName::AbortError);
472        self.error.set(Some(&exception));
473
474        self.terminate_ongoing_reading();
475        // Steps 5 & 6
476        self.dispatch_progress_event(cx, atom!("abort"), 0, None);
477        self.dispatch_progress_event(cx, atom!("loadend"), 0, None);
478    }
479
480    /// <https://w3c.github.io/FileAPI/#dfn-error>
481    fn GetError(&self) -> Option<DomRoot<DOMException>> {
482        self.error.get()
483    }
484
485    #[expect(unsafe_code)]
486    /// <https://w3c.github.io/FileAPI/#dfn-result>
487    fn GetResult(&self) -> Option<StringOrObject> {
488        self.result.borrow().as_ref().map(|r| match *r {
489            FileReaderResult::String(ref string) => StringOrObject::String(string.clone()),
490            FileReaderResult::ArrayBuffer(ref arr_buffer) => {
491                let result = RootedTraceableBox::new(Heap::default());
492                unsafe {
493                    result.set((*arr_buffer.ptr.get()).to_object());
494                }
495                StringOrObject::Object(result)
496            },
497        })
498    }
499
500    /// <https://w3c.github.io/FileAPI/#dfn-readyState>
501    fn ReadyState(&self) -> u16 {
502        self.ready_state.get() as u16
503    }
504}
505
506impl FileReader {
507    fn dispatch_progress_event(
508        &self,
509        cx: &mut js::context::JSContext,
510        type_: Atom,
511        loaded: u64,
512        total: Option<u64>,
513    ) {
514        let progressevent = ProgressEvent::new(
515            cx,
516            &self.global(),
517            type_,
518            EventBubbles::DoesNotBubble,
519            EventCancelable::NotCancelable,
520            total.is_some(),
521            Finite::wrap(loaded as f64),
522            Finite::wrap(total.unwrap_or(0) as f64),
523        );
524        progressevent.upcast::<Event>().fire(cx, self.upcast());
525    }
526
527    fn terminate_ongoing_reading(&self) {
528        let GenerationId(prev_id) = self.generation_id.get();
529        self.generation_id.set(GenerationId(prev_id + 1));
530    }
531
532    /// <https://w3c.github.io/FileAPI/#readOperation>
533    fn read(
534        &self,
535        cx: &mut js::context::JSContext,
536        function: FileReaderFunction,
537        blob: &Blob,
538        encoding: Option<DOMString>,
539    ) -> ErrorResult {
540        // If fr’s state is "loading", throw an InvalidStateError DOMException.
541        if self.ready_state.get() == FileReaderReadyState::Loading {
542            return Err(Error::InvalidState(None));
543        }
544
545        // Set fr’s state to "loading".
546        self.change_ready_state(FileReaderReadyState::Loading);
547
548        // Set fr’s result to null.
549        *self.result.borrow_mut() = None;
550
551        // Set fr’s error to null.
552        // See the note below in the error steps.
553
554        // Let stream be the result of calling get stream on blob.
555        let stream = blob.get_stream(cx);
556
557        // Let reader be the result of getting a reader from stream.
558        let reader = stream.and_then(|s| s.acquire_default_reader(cx))?;
559
560        let load_data = ReadMetaData::new(
561            String::from(blob.Type()),
562            encoding.map(String::from),
563            function,
564        );
565
566        let GenerationId(prev_id) = self.generation_id.get();
567        self.generation_id.set(GenerationId(prev_id + 1));
568        let gen_id = self.generation_id.get();
569
570        let filereader_success = DomRoot::from_ref(self);
571        let filereader_error = DomRoot::from_ref(self);
572
573        // In parallel, while true:
574        // Wait for chunkPromise to be fulfilled or rejected.
575        // Note: the spec appears wrong or outdated,
576        // so for now we use the simple `read_all_bytes` call,
577        // which means we cannot fire the progress event at each chunk.
578        // This can be revisisted following the discussion at
579        // <https://github.com/w3c/FileAPI/issues/208>
580
581        // Read all bytes from stream with reader.
582        reader.read_all_bytes(
583            cx,
584            Rc::new(move |_cx, blob_contents| {
585                let global = filereader_success.global();
586                let task_manager = global.task_manager();
587                let task_source = task_manager.file_reading_task_source();
588
589                // If chunkPromise is fulfilled,
590                // and isFirstChunk is true,
591                // queue a task
592                // Note: this should be done for the first chunk,
593                // see issue above.
594                task_source.queue(FileReadingTask::ProcessRead(
595                    Trusted::new(&filereader_success.clone()),
596                    gen_id,
597                ));
598                // If chunkPromise is fulfilled
599                // with an object whose done property is false
600                // and whose value property is a Uint8Array object
601                // Note: this should be done for each chunk,
602                // see issue above.
603                if !blob_contents.is_empty() {
604                    task_source.queue(FileReadingTask::ProcessReadData(
605                        Trusted::new(&filereader_success.clone()),
606                        gen_id,
607                    ));
608                }
609                // Otherwise,
610                // if chunkPromise is fulfilled with an object whose done property is true,
611                // queue a task
612                // Note: we are in the succes steps of `read_all_bytes`,
613                // so the last chunk has been received.
614                task_source.queue(FileReadingTask::ProcessReadEOF(
615                    Trusted::new(&filereader_success.clone()),
616                    gen_id,
617                    load_data.clone(),
618                    blob_contents.to_vec(),
619                ));
620            }),
621            Rc::new(move |_cx, _error| {
622                let global = filereader_error.global();
623                let task_manager = global.task_manager();
624                let task_source = task_manager.file_reading_task_source();
625
626                // Otherwise, if chunkPromise is rejected with an error error,
627                // queue a task
628                // Note: not using the error from `read_all_bytes`,
629                // see issue above.
630                task_source.queue(FileReadingTask::ProcessReadError(
631                    Trusted::new(&filereader_error),
632                    gen_id,
633                    DOMErrorName::OperationError,
634                ));
635            }),
636        );
637        Ok(())
638    }
639
640    fn change_ready_state(&self, state: FileReaderReadyState) {
641        self.ready_state.set(state);
642    }
643}