script/dom/
filereader.rs

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