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