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