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