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