| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use std::cell::Cell; |
| use std::ptr; |
| use std::rc::Rc; |
| |
| use base64::Engine; |
| use dom_struct::dom_struct; |
| use encoding_rs::{Encoding, UTF_8}; |
| use js::jsapi::{Heap, JSObject}; |
| use js::jsval::{self, JSVal}; |
| use js::rust::HandleObject; |
| use js::typedarray::{ArrayBuffer, CreateWith}; |
| use mime::{self, Mime}; |
| use script_bindings::num::Finite; |
| use stylo_atoms::Atom; |
| |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods; |
| use crate::dom::bindings::codegen::Bindings::FileReaderBinding::{ |
| FileReaderConstants, FileReaderMethods, |
| }; |
| use crate::dom::bindings::codegen::UnionTypes::StringOrObject; |
| use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::refcounted::Trusted; |
| use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; |
| use crate::dom::bindings::root::{DomRoot, MutNullableDom}; |
| use crate::dom::bindings::str::DOMString; |
| use crate::dom::bindings::trace::RootedTraceableBox; |
| use crate::dom::blob::Blob; |
| use crate::dom::domexception::{DOMErrorName, DOMException}; |
| use crate::dom::event::{Event, EventBubbles, EventCancelable}; |
| use crate::dom::eventtarget::EventTarget; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::progressevent::ProgressEvent; |
| use crate::realms::{InRealm, enter_realm}; |
| use crate::script_runtime::{CanGc, JSContext}; |
| use crate::task::TaskOnce; |
| |
| #[allow(dead_code)] |
| pub(crate) enum FileReadingTask { |
| ProcessRead(TrustedFileReader, GenerationId), |
| ProcessReadData(TrustedFileReader, GenerationId), |
| ProcessReadError(TrustedFileReader, GenerationId, DOMErrorName), |
| ProcessReadEOF(TrustedFileReader, GenerationId, ReadMetaData, Vec<u8>), |
| } |
| |
| impl TaskOnce for FileReadingTask { |
| fn run_once(self) { |
| self.handle_task(CanGc::note()); |
| } |
| } |
| |
| impl FileReadingTask { |
| pub(crate) fn handle_task(self, can_gc: CanGc) { |
| use self::FileReadingTask::*; |
| |
| match self { |
| ProcessRead(reader, gen_id) => FileReader::process_read(reader, gen_id, can_gc), |
| ProcessReadData(reader, gen_id) => { |
| FileReader::process_read_data(reader, gen_id, can_gc) |
| }, |
| ProcessReadError(reader, gen_id, error) => { |
| FileReader::process_read_error(reader, gen_id, error, can_gc) |
| }, |
| ProcessReadEOF(reader, gen_id, metadata, blob_contents) => { |
| FileReader::process_read_eof(reader, gen_id, metadata, blob_contents, can_gc) |
| }, |
| } |
| } |
| } |
| #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] |
| pub(crate) enum FileReaderFunction { |
| Text, |
| DataUrl, |
| ArrayBuffer, |
| } |
| |
| pub(crate) type TrustedFileReader = Trusted<FileReader>; |
| |
| #[derive(Clone, MallocSizeOf)] |
| pub(crate) struct ReadMetaData { |
| pub(crate) blobtype: String, |
| pub(crate) label: Option<String>, |
| pub(crate) function: FileReaderFunction, |
| } |
| |
| impl ReadMetaData { |
| pub(crate) fn new( |
| blobtype: String, |
| label: Option<String>, |
| function: FileReaderFunction, |
| ) -> ReadMetaData { |
| ReadMetaData { |
| blobtype, |
| label, |
| function, |
| } |
| } |
| } |
| |
| #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] |
| pub(crate) struct GenerationId(u32); |
| |
| #[repr(u16)] |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] |
| pub(crate) enum FileReaderReadyState { |
| Empty = FileReaderConstants::EMPTY, |
| Loading = FileReaderConstants::LOADING, |
| Done = FileReaderConstants::DONE, |
| } |
| |
| #[derive(JSTraceable, MallocSizeOf)] |
| pub(crate) enum FileReaderResult { |
| ArrayBuffer(#[ignore_malloc_size_of = "mozjs"] RootedTraceableBox<Heap<JSVal>>), |
| String(DOMString), |
| } |
| |
| pub(crate) struct FileReaderSharedFunctionality; |
| |
| impl FileReaderSharedFunctionality { |
| pub(crate) fn dataurl_format(blob_contents: &[u8], blob_type: String) -> DOMString { |
| let base64 = base64::engine::general_purpose::STANDARD.encode(blob_contents); |
| |
| let dataurl = if blob_type.is_empty() { |
| format!("data:base64,{}", base64) |
| } else { |
| format!("data:{};base64,{}", blob_type, base64) |
| }; |
| |
| DOMString::from(dataurl) |
| } |
| |
| pub(crate) fn text_decode( |
| blob_contents: &[u8], |
| blob_type: &str, |
| blob_label: &Option<String>, |
| ) -> DOMString { |
| // https://w3c.github.io/FileAPI/#encoding-determination |
| // Steps 1 & 2 & 3 |
| let mut encoding = blob_label |
| .as_ref() |
| .map(|string| string.as_bytes()) |
| .and_then(Encoding::for_label); |
| |
| // Step 4 & 5 |
| encoding = encoding.or_else(|| { |
| let resultmime = blob_type.parse::<Mime>().ok(); |
| resultmime.and_then(|mime| { |
| mime.params() |
| .find(|(k, _)| &mime::CHARSET == k) |
| .and_then(|(_, v)| Encoding::for_label(v.as_ref().as_bytes())) |
| }) |
| }); |
| |
| // Step 6 |
| let enc = encoding.unwrap_or(UTF_8); |
| |
| let convert = blob_contents; |
| // Step 7 |
| let (output, _, _) = enc.decode(convert); |
| DOMString::from(output) |
| } |
| } |
| |
| #[dom_struct] |
| pub(crate) struct FileReader { |
| eventtarget: EventTarget, |
| ready_state: Cell<FileReaderReadyState>, |
| error: MutNullableDom<DOMException>, |
| result: DomRefCell<Option<FileReaderResult>>, |
| generation_id: Cell<GenerationId>, |
| } |
| |
| impl FileReader { |
| pub(crate) fn new_inherited() -> FileReader { |
| FileReader { |
| eventtarget: EventTarget::new_inherited(), |
| ready_state: Cell::new(FileReaderReadyState::Empty), |
| error: MutNullableDom::new(None), |
| result: DomRefCell::new(None), |
| generation_id: Cell::new(GenerationId(0)), |
| } |
| } |
| |
| fn new( |
| global: &GlobalScope, |
| proto: Option<HandleObject>, |
| can_gc: CanGc, |
| ) -> DomRoot<FileReader> { |
| reflect_dom_object_with_proto(Box::new(FileReader::new_inherited()), global, proto, can_gc) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-error-steps |
| pub(crate) fn process_read_error( |
| filereader: TrustedFileReader, |
| gen_id: GenerationId, |
| error: DOMErrorName, |
| can_gc: CanGc, |
| ) { |
| let fr = filereader.root(); |
| |
| macro_rules! return_on_abort( |
| () => ( |
| if gen_id != fr.generation_id.get() { |
| return |
| } |
| ); |
| ); |
| |
| return_on_abort!(); |
| // Step 1 |
| fr.change_ready_state(FileReaderReadyState::Done); |
| *fr.result.borrow_mut() = None; |
| |
| let exception = DOMException::new(&fr.global(), error, can_gc); |
| fr.error.set(Some(&exception)); |
| |
| fr.dispatch_progress_event(atom!("error"), 0, None, can_gc); |
| return_on_abort!(); |
| // Step 3 |
| fr.dispatch_progress_event(atom!("loadend"), 0, None, can_gc); |
| return_on_abort!(); |
| // Step 4 |
| fr.terminate_ongoing_reading(); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsText |
| pub(crate) fn process_read_data( |
| filereader: TrustedFileReader, |
| gen_id: GenerationId, |
| can_gc: CanGc, |
| ) { |
| let fr = filereader.root(); |
| |
| macro_rules! return_on_abort( |
| () => ( |
| if gen_id != fr.generation_id.get() { |
| return |
| } |
| ); |
| ); |
| return_on_abort!(); |
| // FIXME Step 7 send current progress |
| fr.dispatch_progress_event(atom!("progress"), 0, None, can_gc); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsText |
| pub(crate) fn process_read(filereader: TrustedFileReader, gen_id: GenerationId, can_gc: CanGc) { |
| let fr = filereader.root(); |
| |
| macro_rules! return_on_abort( |
| () => ( |
| if gen_id != fr.generation_id.get() { |
| return |
| } |
| ); |
| ); |
| return_on_abort!(); |
| // Step 6 |
| fr.dispatch_progress_event(atom!("loadstart"), 0, None, can_gc); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsText |
| pub(crate) fn process_read_eof( |
| filereader: TrustedFileReader, |
| gen_id: GenerationId, |
| data: ReadMetaData, |
| blob_contents: Vec<u8>, |
| can_gc: CanGc, |
| ) { |
| let fr = filereader.root(); |
| |
| macro_rules! return_on_abort( |
| () => ( |
| if gen_id != fr.generation_id.get() { |
| return |
| } |
| ); |
| ); |
| |
| return_on_abort!(); |
| // Step 8.1 |
| fr.change_ready_state(FileReaderReadyState::Done); |
| // Step 8.2 |
| |
| match data.function { |
| FileReaderFunction::DataUrl => { |
| FileReader::perform_readasdataurl(&fr.result, data, &blob_contents) |
| }, |
| FileReaderFunction::Text => { |
| FileReader::perform_readastext(&fr.result, data, &blob_contents) |
| }, |
| FileReaderFunction::ArrayBuffer => { |
| let _ac = enter_realm(&*fr); |
| FileReader::perform_readasarraybuffer( |
| &fr.result, |
| GlobalScope::get_cx(), |
| data, |
| &blob_contents, |
| ) |
| }, |
| }; |
| |
| // Step 8.3 |
| fr.dispatch_progress_event(atom!("load"), 0, None, can_gc); |
| return_on_abort!(); |
| // Step 8.4 |
| if fr.ready_state.get() != FileReaderReadyState::Loading { |
| fr.dispatch_progress_event(atom!("loadend"), 0, None, can_gc); |
| } |
| return_on_abort!(); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsText |
| fn perform_readastext( |
| result: &DomRefCell<Option<FileReaderResult>>, |
| data: ReadMetaData, |
| blob_bytes: &[u8], |
| ) { |
| let blob_label = &data.label; |
| let blob_type = &data.blobtype; |
| |
| let output = FileReaderSharedFunctionality::text_decode(blob_bytes, blob_type, blob_label); |
| *result.borrow_mut() = Some(FileReaderResult::String(output)); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsDataURL |
| fn perform_readasdataurl( |
| result: &DomRefCell<Option<FileReaderResult>>, |
| data: ReadMetaData, |
| bytes: &[u8], |
| ) { |
| let output = FileReaderSharedFunctionality::dataurl_format(bytes, data.blobtype); |
| |
| *result.borrow_mut() = Some(FileReaderResult::String(output)); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer |
| #[allow(unsafe_code)] |
| fn perform_readasarraybuffer( |
| result: &DomRefCell<Option<FileReaderResult>>, |
| cx: JSContext, |
| _: ReadMetaData, |
| bytes: &[u8], |
| ) { |
| unsafe { |
| rooted!(in(*cx) let mut array_buffer = ptr::null_mut::<JSObject>()); |
| assert!( |
| ArrayBuffer::create(*cx, CreateWith::Slice(bytes), array_buffer.handle_mut()) |
| .is_ok() |
| ); |
| |
| *result.borrow_mut() = |
| Some(FileReaderResult::ArrayBuffer(RootedTraceableBox::default())); |
| |
| if let Some(FileReaderResult::ArrayBuffer(ref mut heap)) = *result.borrow_mut() { |
| heap.set(jsval::ObjectValue(array_buffer.get())); |
| }; |
| } |
| } |
| } |
| |
| impl FileReaderMethods<crate::DomTypeHolder> for FileReader { |
| // https://w3c.github.io/FileAPI/#filereaderConstrctr |
| fn Constructor( |
| global: &GlobalScope, |
| proto: Option<HandleObject>, |
| can_gc: CanGc, |
| ) -> Fallible<DomRoot<FileReader>> { |
| Ok(FileReader::new(global, proto, can_gc)) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-onloadstart |
| event_handler!(loadstart, GetOnloadstart, SetOnloadstart); |
| |
| // https://w3c.github.io/FileAPI/#dfn-onprogress |
| event_handler!(progress, GetOnprogress, SetOnprogress); |
| |
| // https://w3c.github.io/FileAPI/#dfn-onload |
| event_handler!(load, GetOnload, SetOnload); |
| |
| // https://w3c.github.io/FileAPI/#dfn-onabort |
| event_handler!(abort, GetOnabort, SetOnabort); |
| |
| // https://w3c.github.io/FileAPI/#dfn-onerror |
| event_handler!(error, GetOnerror, SetOnerror); |
| |
| // https://w3c.github.io/FileAPI/#dfn-onloadend |
| event_handler!(loadend, GetOnloadend, SetOnloadend); |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer |
| fn ReadAsArrayBuffer(&self, blob: &Blob, realm: InRealm, can_gc: CanGc) -> ErrorResult { |
| self.read(FileReaderFunction::ArrayBuffer, blob, None, realm, can_gc) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsDataURL |
| fn ReadAsDataURL(&self, blob: &Blob, realm: InRealm, can_gc: CanGc) -> ErrorResult { |
| self.read(FileReaderFunction::DataUrl, blob, None, realm, can_gc) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readAsText |
| fn ReadAsText( |
| &self, |
| blob: &Blob, |
| label: Option<DOMString>, |
| realm: InRealm, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| self.read(FileReaderFunction::Text, blob, label, realm, can_gc) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-abort |
| fn Abort(&self, can_gc: CanGc) { |
| // Step 2 |
| if self.ready_state.get() == FileReaderReadyState::Loading { |
| self.change_ready_state(FileReaderReadyState::Done); |
| } |
| // Steps 1 & 3 |
| *self.result.borrow_mut() = None; |
| |
| let exception = DOMException::new(&self.global(), DOMErrorName::AbortError, can_gc); |
| self.error.set(Some(&exception)); |
| |
| self.terminate_ongoing_reading(); |
| // Steps 5 & 6 |
| self.dispatch_progress_event(atom!("abort"), 0, None, can_gc); |
| self.dispatch_progress_event(atom!("loadend"), 0, None, can_gc); |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-error |
| fn GetError(&self) -> Option<DomRoot<DOMException>> { |
| self.error.get() |
| } |
| |
| #[allow(unsafe_code)] |
| // https://w3c.github.io/FileAPI/#dfn-result |
| fn GetResult(&self, _: JSContext) -> Option<StringOrObject> { |
| self.result.borrow().as_ref().map(|r| match *r { |
| FileReaderResult::String(ref string) => StringOrObject::String(string.clone()), |
| FileReaderResult::ArrayBuffer(ref arr_buffer) => { |
| let result = RootedTraceableBox::new(Heap::default()); |
| unsafe { |
| result.set((*arr_buffer.ptr.get()).to_object()); |
| } |
| StringOrObject::Object(result) |
| }, |
| }) |
| } |
| |
| // https://w3c.github.io/FileAPI/#dfn-readyState |
| fn ReadyState(&self) -> u16 { |
| self.ready_state.get() as u16 |
| } |
| } |
| |
| impl FileReader { |
| fn dispatch_progress_event(&self, type_: Atom, loaded: u64, total: Option<u64>, can_gc: CanGc) { |
| let progressevent = ProgressEvent::new( |
| &self.global(), |
| type_, |
| EventBubbles::DoesNotBubble, |
| EventCancelable::NotCancelable, |
| total.is_some(), |
| Finite::wrap(loaded as f64), |
| Finite::wrap(total.unwrap_or(0) as f64), |
| can_gc, |
| ); |
| progressevent.upcast::<Event>().fire(self.upcast(), can_gc); |
| } |
| |
| fn terminate_ongoing_reading(&self) { |
| let GenerationId(prev_id) = self.generation_id.get(); |
| self.generation_id.set(GenerationId(prev_id + 1)); |
| } |
| |
| /// <https://w3c.github.io/FileAPI/#readOperation> |
| fn read( |
| &self, |
| function: FileReaderFunction, |
| blob: &Blob, |
| label: Option<DOMString>, |
| realm: InRealm, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| let cx = GlobalScope::get_cx(); |
| |
| // If fr’s state is "loading", throw an InvalidStateError DOMException. |
| if self.ready_state.get() == FileReaderReadyState::Loading { |
| return Err(Error::InvalidState); |
| } |
| |
| // Set fr’s state to "loading". |
| self.change_ready_state(FileReaderReadyState::Loading); |
| |
| // Set fr’s result to null. |
| *self.result.borrow_mut() = None; |
| |
| // Set fr’s error to null. |
| // See the note below in the error steps. |
| |
| // Let stream be the result of calling get stream on blob. |
| let stream = blob.get_stream(can_gc); |
| |
| // Let reader be the result of getting a reader from stream. |
| let reader = stream.and_then(|s| s.acquire_default_reader(can_gc))?; |
| |
| let type_ = blob.Type(); |
| |
| let load_data = ReadMetaData::new(String::from(type_), label.map(String::from), function); |
| |
| let GenerationId(prev_id) = self.generation_id.get(); |
| self.generation_id.set(GenerationId(prev_id + 1)); |
| let gen_id = self.generation_id.get(); |
| |
| let filereader_success = DomRoot::from_ref(self); |
| let filereader_error = DomRoot::from_ref(self); |
| |
| // In parallel, while true: |
| // Wait for chunkPromise to be fulfilled or rejected. |
| // Note: the spec appears wrong or outdated, |
| // so for now we use the simple `read_all_bytes` call, |
| // which means we cannot fire the progress event at each chunk. |
| // This can be revisisted following the discussion at |
| // <https://github.com/w3c/FileAPI/issues/208> |
| |
| // Read all bytes from stream with reader. |
| reader.read_all_bytes( |
| cx, |
| &self.global(), |
| Rc::new(move |blob_contents| { |
| let global = filereader_success.global(); |
| let task_manager = global.task_manager(); |
| let task_source = task_manager.file_reading_task_source(); |
| |
| // If chunkPromise is fulfilled, |
| // and isFirstChunk is true, |
| // queue a task |
| // Note: this should be done for the first chunk, |
| // see issue above. |
| task_source.queue(FileReadingTask::ProcessRead( |
| Trusted::new(&filereader_success.clone()), |
| gen_id, |
| )); |
| // If chunkPromise is fulfilled |
| // with an object whose done property is false |
| // and whose value property is a Uint8Array object |
| // Note: this should be done for each chunk, |
| // see issue above. |
| if !blob_contents.is_empty() { |
| task_source.queue(FileReadingTask::ProcessReadData( |
| Trusted::new(&filereader_success.clone()), |
| gen_id, |
| )); |
| } |
| // Otherwise, |
| // if chunkPromise is fulfilled with an object whose done property is true, |
| // queue a task |
| // Note: we are in the succes steps of `read_all_bytes`, |
| // so the last chunk has been received. |
| task_source.queue(FileReadingTask::ProcessReadEOF( |
| Trusted::new(&filereader_success.clone()), |
| gen_id, |
| load_data.clone(), |
| blob_contents.to_vec(), |
| )); |
| }), |
| Rc::new(move |_cx, _error| { |
| let global = filereader_error.global(); |
| let task_manager = global.task_manager(); |
| let task_source = task_manager.file_reading_task_source(); |
| |
| // Otherwise, if chunkPromise is rejected with an error error, |
| // queue a task |
| // Note: not using the error from `read_all_bytes`, |
| // see issue above. |
| task_source.queue(FileReadingTask::ProcessReadError( |
| Trusted::new(&filereader_error), |
| gen_id, |
| DOMErrorName::OperationError, |
| )); |
| }), |
| realm, |
| can_gc, |
| ); |
| Ok(()) |
| } |
| |
| fn change_ready_state(&self, state: FileReaderReadyState) { |
| self.ready_state.set(state); |
| } |
| } |