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::domstring::TracedDOMString;
19use script_bindings::num::Finite;
20use script_bindings::reflector::reflect_dom_object_with_proto;
21use stylo_atoms::Atom;
22
23use crate::dom::bindings::buffer_source::create_buffer_source;
24use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
25use crate::dom::bindings::codegen::Bindings::FileReaderBinding::{
26    FileReaderConstants, FileReaderMethods,
27};
28use crate::dom::bindings::codegen::UnionTypes::StringOrObject;
29use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::DomGlobal;
33use crate::dom::bindings::root::{DomRoot, MutNullableDom};
34use crate::dom::bindings::str::DOMString;
35use crate::dom::bindings::trace::RootedTraceableBox;
36use crate::dom::blob::Blob;
37use crate::dom::domexception::{DOMErrorName, DOMException};
38use crate::dom::event::{Event, EventBubbles, EventCancelable};
39use crate::dom::eventtarget::EventTarget;
40use crate::dom::globalscope::GlobalScope;
41use crate::dom::progressevent::ProgressEvent;
42use crate::realms::enter_auto_realm;
43use crate::tasks::task::TaskOnce;
44
45pub(crate) enum FileReadingTask {
46    ProcessRead(TrustedFileReader, GenerationId),
47    ProcessReadData(TrustedFileReader, GenerationId),
48    ProcessReadError(TrustedFileReader, GenerationId, DOMErrorName),
49    ProcessReadEOF(TrustedFileReader, GenerationId, ReadMetaData, Vec<u8>),
50}
51
52impl TaskOnce for FileReadingTask {
53    fn run_once(self, cx: &mut js::context::JSContext) {
54        self.handle_task(cx);
55    }
56}
57
58impl FileReadingTask {
59    pub(crate) fn handle_task(self, cx: &mut js::context::JSContext) {
60        use self::FileReadingTask::*;
61
62        match self {
63            ProcessRead(reader, gen_id) => FileReader::process_read(cx, reader, gen_id),
64            ProcessReadData(reader, gen_id) => FileReader::process_read_data(cx, reader, gen_id),
65            ProcessReadError(reader, gen_id, error) => {
66                FileReader::process_read_error(cx, reader, gen_id, error)
67            },
68            ProcessReadEOF(reader, gen_id, metadata, blob_contents) => {
69                FileReader::process_read_eof(cx, reader, gen_id, metadata, blob_contents)
70            },
71        }
72    }
73}
74#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
75pub(crate) enum FileReaderFunction {
76    Text,
77    DataUrl,
78    ArrayBuffer,
79    BinaryString,
80}
81
82pub(crate) type TrustedFileReader = Trusted<FileReader>;
83
84#[derive(Clone, MallocSizeOf)]
85pub(crate) struct ReadMetaData {
86    pub(crate) blobtype: String,
87    pub(crate) encoding: Option<String>,
88    pub(crate) function: FileReaderFunction,
89}
90
91impl ReadMetaData {
92    pub(crate) fn new(
93        blobtype: String,
94        encoding: Option<String>,
95        function: FileReaderFunction,
96    ) -> ReadMetaData {
97        ReadMetaData {
98            blobtype,
99            encoding,
100            function,
101        }
102    }
103}
104
105#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
106pub(crate) struct GenerationId(u32);
107
108#[repr(u16)]
109#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
110pub(crate) enum FileReaderReadyState {
111    Empty = FileReaderConstants::EMPTY,
112    Loading = FileReaderConstants::LOADING,
113    Done = FileReaderConstants::DONE,
114}
115
116#[derive(JSTraceable, MallocSizeOf)]
117#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
118pub(crate) enum FileReaderResult {
119    ArrayBuffer(#[ignore_malloc_size_of = "mozjs"] Heap<JSVal>),
120    String(TracedDOMString),
121}
122
123pub(crate) struct FileReaderSharedFunctionality;
124
125impl FileReaderSharedFunctionality {
126    /// <https://w3c.github.io/FileAPI/#blob-package-data>
127    pub(crate) fn dataurl_for_bytes(bytes: &[u8], blob_type: &str) -> DOMString {
128        // If mimeType (blobType) is not available return a Data URL without a media-type. [RFC2397].
129        // Spec says a Data URL without a media-type when blob_type is unavailable.
130        // However, all other browsers use "application/octet-stream" in this case.
131        let mime_type = if blob_type.is_empty() {
132            "application/octet-stream"
133        } else {
134            blob_type
135        };
136
137        Self::dataurl_format(bytes, mime_type)
138    }
139
140    /// [RFC2397]
141    /// <https://www.rfc-editor.org/rfc/rfc2397.html>
142    fn dataurl_format(bytes: &[u8], mime_type: &str) -> DOMString {
143        let base64 = base64::engine::general_purpose::STANDARD.encode(bytes);
144        let dataurl = format!("data:{};base64,{}", mime_type, base64);
145
146        DOMString::from(dataurl)
147    }
148
149    /// <https://w3c.github.io/FileAPI/#blob-package-data>
150    pub(crate) fn binary_string_for_bytes(bytes: &[u8]) -> DOMString {
151        DOMString::from(bytes.iter().map(|&byte| byte as char).collect::<String>())
152    }
153
154    /// <https://w3c.github.io/FileAPI/#blob-package-data>
155    pub(crate) fn text_for_bytes(
156        bytes: &[u8],
157        blob_type: &str,
158        encoding: &Option<String>,
159    ) -> DOMString {
160        // https://w3c.github.io/FileAPI/#encoding-determination
161        // FIXME: This url is non-existent. Fixing later...
162        // Steps 1 & 2 & 3
163        let mut encoding = encoding
164            .as_ref()
165            .map(|string| string.as_bytes())
166            .and_then(Encoding::for_label);
167
168        // Step 4 & 5
169        encoding = encoding.or_else(|| {
170            let resultmime = blob_type.parse::<Mime>().ok();
171            resultmime.and_then(|mime| {
172                mime.params()
173                    .find(|(k, _)| &mime::CHARSET == k)
174                    .and_then(|(_, v)| Encoding::for_label(v.as_ref().as_bytes()))
175            })
176        });
177
178        // Step 6
179        let enc = encoding.unwrap_or(UTF_8);
180
181        let convert = bytes;
182        // Step 7
183        // https://encoding.spec.whatwg.org/#decode
184        let (output, _, _) = enc.decode(convert);
185        DOMString::from(output)
186    }
187}
188
189#[dom_struct]
190pub(crate) struct FileReader {
191    eventtarget: EventTarget,
192    ready_state: Cell<FileReaderReadyState>,
193    error: MutNullableDom<DOMException>,
194    result: DomRefCell<Option<FileReaderResult>>,
195    generation_id: Cell<GenerationId>,
196}
197
198impl FileReader {
199    pub(crate) fn new_inherited() -> FileReader {
200        FileReader {
201            eventtarget: EventTarget::new_inherited(),
202            ready_state: Cell::new(FileReaderReadyState::Empty),
203            error: MutNullableDom::new(None),
204            result: DomRefCell::new(None),
205            generation_id: Cell::new(GenerationId(0)),
206        }
207    }
208
209    fn new(
210        cx: &mut js::context::JSContext,
211        global: &GlobalScope,
212        proto: Option<HandleObject>,
213    ) -> DomRoot<FileReader> {
214        reflect_dom_object_with_proto(cx, Box::new(FileReader::new_inherited()), global, proto)
215    }
216
217    /// <https://w3c.github.io/FileAPI/#dfn-error-steps>
218    pub(crate) fn process_read_error(
219        cx: &mut js::context::JSContext,
220        filereader: TrustedFileReader,
221        gen_id: GenerationId,
222        error: DOMErrorName,
223    ) {
224        let fr = filereader.root();
225
226        macro_rules! return_on_abort(
227            () => (
228                if gen_id != fr.generation_id.get() {
229                    return
230                }
231            );
232        );
233
234        return_on_abort!();
235        // Step 1
236        fr.change_ready_state(FileReaderReadyState::Done);
237        *fr.result.borrow_mut() = None;
238
239        let exception = DOMException::new(cx, &fr.global(), error);
240        fr.error.set(Some(&exception));
241
242        fr.dispatch_progress_event(cx, atom!("error"), 0, None);
243        return_on_abort!();
244        // Step 3
245        fr.dispatch_progress_event(cx, atom!("loadend"), 0, None);
246        return_on_abort!();
247        // Step 4
248        fr.terminate_ongoing_reading();
249    }
250
251    /// <https://w3c.github.io/FileAPI/#dfn-readAsText>
252    pub(crate) fn process_read_data(
253        cx: &mut js::context::JSContext,
254        filereader: TrustedFileReader,
255        gen_id: GenerationId,
256    ) {
257        let fr = filereader.root();
258
259        macro_rules! return_on_abort(
260            () => (
261                if gen_id != fr.generation_id.get() {
262                    return
263                }
264            );
265        );
266        return_on_abort!();
267        // FIXME Step 7 send current progress
268        fr.dispatch_progress_event(cx, atom!("progress"), 0, None);
269    }
270
271    /// <https://w3c.github.io/FileAPI/#dfn-readAsText>
272    pub(crate) fn process_read(
273        cx: &mut js::context::JSContext,
274        filereader: TrustedFileReader,
275        gen_id: GenerationId,
276    ) {
277        let fr = filereader.root();
278
279        macro_rules! return_on_abort(
280            () => (
281                if gen_id != fr.generation_id.get() {
282                    return
283                }
284            );
285        );
286        return_on_abort!();
287        // Step 6
288        fr.dispatch_progress_event(cx, atom!("loadstart"), 0, None);
289    }
290
291    /// <https://w3c.github.io/FileAPI/#readOperation>
292    pub(crate) fn process_read_eof(
293        cx: &mut js::context::JSContext,
294        filereader: TrustedFileReader,
295        gen_id: GenerationId,
296        data: ReadMetaData,
297        blob_contents: Vec<u8>,
298    ) {
299        let fr = filereader.root();
300
301        macro_rules! return_on_abort(
302            () => (
303                if gen_id != fr.generation_id.get() {
304                    return
305                }
306            );
307        );
308
309        return_on_abort!();
310        // Step 8.1
311        fr.change_ready_state(FileReaderReadyState::Done);
312
313        // Step 10.5.2: Let result be the result of package data given bytes,
314        // type, blob’s type, and encodingName.
315
316        // <https://w3c.github.io/FileAPI/#blob-package-data>
317        match data.function {
318            FileReaderFunction::DataUrl => {
319                FileReader::perform_readasdataurl(&fr.result, data, &blob_contents)
320            },
321            FileReaderFunction::Text => {
322                FileReader::perform_readastext(&fr.result, data, &blob_contents)
323            },
324            FileReaderFunction::ArrayBuffer => {
325                let mut realm = enter_auto_realm(cx, &*fr);
326                let cx = &mut realm.current_realm();
327                FileReader::perform_readasarraybuffer(cx, &fr.result, &blob_contents)
328            },
329            FileReaderFunction::BinaryString => {
330                FileReader::perform_readasbinarystring(&fr.result, &blob_contents)
331            },
332        };
333
334        // Step 8.3
335        fr.dispatch_progress_event(cx, atom!("load"), 0, None);
336        return_on_abort!();
337        // Step 8.4
338        if fr.ready_state.get() != FileReaderReadyState::Loading {
339            fr.dispatch_progress_event(cx, atom!("loadend"), 0, None);
340        }
341        return_on_abort!();
342    }
343
344    /// <https://w3c.github.io/FileAPI/#packaging-data>
345    fn perform_readastext(
346        result: &DomRefCell<Option<FileReaderResult>>,
347        data: ReadMetaData,
348        blob_bytes: &[u8],
349    ) {
350        *result.borrow_mut() = Some(FileReaderResult::String(
351            FileReaderSharedFunctionality::text_for_bytes(
352                blob_bytes,
353                &data.blobtype,
354                &data.encoding,
355            )
356            .traced(),
357        ));
358    }
359
360    /// <https://w3c.github.io/FileAPI/#packaging-data>
361    fn perform_readasdataurl(
362        result: &DomRefCell<Option<FileReaderResult>>,
363        data: ReadMetaData,
364        bytes: &[u8],
365    ) {
366        *result.borrow_mut() = Some(FileReaderResult::String(
367            FileReaderSharedFunctionality::dataurl_for_bytes(bytes, &data.blobtype).traced(),
368        ));
369    }
370
371    /// <https://w3c.github.io/FileAPI/#packaging-data>
372    /// > Return bytes as a binary string, in which every byte
373    /// > is represented by a code unit of equal value [0..255].
374    fn perform_readasbinarystring(result: &DomRefCell<Option<FileReaderResult>>, bytes: &[u8]) {
375        *result.borrow_mut() = Some(FileReaderResult::String(
376            FileReaderSharedFunctionality::binary_string_for_bytes(bytes).traced(),
377        ));
378    }
379
380    /// <https://w3c.github.io/FileAPI/#packaging-data>
381    /// > Return a new ArrayBuffer whose contents are bytes.
382    fn perform_readasarraybuffer(
383        cx: &mut js::context::JSContext,
384        result: &DomRefCell<Option<FileReaderResult>>,
385        bytes: &[u8],
386    ) {
387        rooted!(&in(cx) let mut array_buffer = ptr::null_mut::<JSObject>());
388        assert!(
389            create_buffer_source::<ArrayBufferU8>(cx, bytes, array_buffer.handle_mut()).is_ok()
390        );
391
392        *result.borrow_mut() = Some(FileReaderResult::ArrayBuffer(Heap::default()));
393
394        if let Some(FileReaderResult::ArrayBuffer(heap)) = &*result.borrow() {
395            heap.set(jsval::ObjectValue(array_buffer.get()));
396        };
397    }
398}
399
400impl FileReaderMethods<crate::DomTypeHolder> for FileReader {
401    /// <https://w3c.github.io/FileAPI/#filereaderConstrctr>
402    fn Constructor(
403        cx: &mut js::context::JSContext,
404        global: &GlobalScope,
405        proto: Option<HandleObject>,
406    ) -> Fallible<DomRoot<FileReader>> {
407        Ok(FileReader::new(cx, global, proto))
408    }
409
410    // https://w3c.github.io/FileAPI/#dfn-onloadstart
411    event_handler!(loadstart, GetOnloadstart, SetOnloadstart);
412
413    // https://w3c.github.io/FileAPI/#dfn-onprogress
414    event_handler!(progress, GetOnprogress, SetOnprogress);
415
416    // https://w3c.github.io/FileAPI/#dfn-onload
417    event_handler!(load, GetOnload, SetOnload);
418
419    // https://w3c.github.io/FileAPI/#dfn-onabort
420    event_handler!(abort, GetOnabort, SetOnabort);
421
422    // https://w3c.github.io/FileAPI/#dfn-onerror
423    event_handler!(error, GetOnerror, SetOnerror);
424
425    // https://w3c.github.io/FileAPI/#dfn-onloadend
426    event_handler!(loadend, GetOnloadend, SetOnloadend);
427
428    /// <https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer>
429    fn ReadAsArrayBuffer(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
430        // > The readAsArrayBuffer(blob) method, when invoked,
431        // must initiate a read operation for blob with ArrayBuffer.
432        self.read(cx, FileReaderFunction::ArrayBuffer, blob, None)
433    }
434
435    /// <https://w3c.github.io/FileAPI/#dfn-readAsBinaryString>
436    fn ReadAsBinaryString(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
437        // > The readAsBinaryString(blob) method, when invoked,
438        // must initiate a read operation for blob with BinaryString.
439        self.read(cx, FileReaderFunction::BinaryString, blob, None)
440    }
441
442    /// <https://w3c.github.io/FileAPI/#dfn-readAsDataURL>
443    fn ReadAsDataURL(&self, cx: &mut js::context::JSContext, blob: &Blob) -> ErrorResult {
444        // > The readAsDataURL(blob) method, when invoked,
445        // must initiate a read operation for blob with DataURL.
446        self.read(cx, FileReaderFunction::DataUrl, blob, None)
447    }
448
449    /// <https://w3c.github.io/FileAPI/#dfn-readAsText>
450    fn ReadAsText(
451        &self,
452        cx: &mut js::context::JSContext,
453        blob: &Blob,
454        encoding: Option<DOMString>,
455    ) -> ErrorResult {
456        // > The readAsText(blob, encoding) method, when invoked,
457        // must initiate a read operation for blob with Text and encoding.
458        self.read(cx, FileReaderFunction::Text, blob, encoding)
459    }
460
461    /// <https://w3c.github.io/FileAPI/#dfn-abort>
462    fn Abort(&self, cx: &mut js::context::JSContext) {
463        // Step 2
464        if self.ready_state.get() == FileReaderReadyState::Loading {
465            self.change_ready_state(FileReaderReadyState::Done);
466        }
467        // Steps 1 & 3
468        *self.result.borrow_mut() = None;
469
470        let exception = DOMException::new(cx, &self.global(), DOMErrorName::AbortError);
471        self.error.set(Some(&exception));
472
473        self.terminate_ongoing_reading();
474        // Steps 5 & 6
475        self.dispatch_progress_event(cx, atom!("abort"), 0, None);
476        self.dispatch_progress_event(cx, atom!("loadend"), 0, None);
477    }
478
479    /// <https://w3c.github.io/FileAPI/#dfn-error>
480    fn GetError(&self) -> Option<DomRoot<DOMException>> {
481        self.error.get()
482    }
483
484    /// <https://w3c.github.io/FileAPI/#dfn-result>
485    fn GetResult(&self) -> Option<StringOrObject> {
486        self.result.borrow().as_ref().map(|r| match *r {
487            FileReaderResult::String(ref string) => StringOrObject::String(string.clone().root()),
488            FileReaderResult::ArrayBuffer(ref array_buffer) => {
489                let result = RootedTraceableBox::new(Heap::default());
490                result.set(array_buffer.get().to_object());
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,
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}