/src/serenity/Userland/Libraries/LibWeb/FileAPI/FileReader.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Shannon Booth <shannon@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Assertions.h> |
8 | | #include <AK/Base64.h> |
9 | | #include <AK/ByteBuffer.h> |
10 | | #include <AK/Time.h> |
11 | | #include <LibJS/Heap/Heap.h> |
12 | | #include <LibJS/Runtime/Promise.h> |
13 | | #include <LibJS/Runtime/Realm.h> |
14 | | #include <LibJS/Runtime/TypedArray.h> |
15 | | #include <LibTextCodec/Decoder.h> |
16 | | #include <LibWeb/Bindings/FileReaderPrototype.h> |
17 | | #include <LibWeb/Bindings/Intrinsics.h> |
18 | | #include <LibWeb/DOM/Event.h> |
19 | | #include <LibWeb/DOM/EventTarget.h> |
20 | | #include <LibWeb/FileAPI/Blob.h> |
21 | | #include <LibWeb/FileAPI/FileReader.h> |
22 | | #include <LibWeb/HTML/EventLoop/EventLoop.h> |
23 | | #include <LibWeb/HTML/EventNames.h> |
24 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
25 | | #include <LibWeb/MimeSniff/MimeType.h> |
26 | | #include <LibWeb/Platform/EventLoopPlugin.h> |
27 | | #include <LibWeb/Streams/AbstractOperations.h> |
28 | | #include <LibWeb/Streams/ReadableStream.h> |
29 | | #include <LibWeb/Streams/ReadableStreamDefaultReader.h> |
30 | | #include <LibWeb/WebIDL/DOMException.h> |
31 | | #include <LibWeb/WebIDL/ExceptionOr.h> |
32 | | |
33 | | namespace Web::FileAPI { |
34 | | |
35 | | JS_DEFINE_ALLOCATOR(FileReader); |
36 | | |
37 | 0 | FileReader::~FileReader() = default; |
38 | | |
39 | | FileReader::FileReader(JS::Realm& realm) |
40 | 0 | : DOM::EventTarget(realm) |
41 | 0 | { |
42 | 0 | } |
43 | | |
44 | | void FileReader::initialize(JS::Realm& realm) |
45 | 0 | { |
46 | 0 | Base::initialize(realm); |
47 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(FileReader); |
48 | 0 | } |
49 | | |
50 | | void FileReader::visit_edges(JS::Cell::Visitor& visitor) |
51 | 0 | { |
52 | 0 | Base::visit_edges(visitor); |
53 | 0 | visitor.visit(m_error); |
54 | 0 | } |
55 | | |
56 | | JS::NonnullGCPtr<FileReader> FileReader::create(JS::Realm& realm) |
57 | 0 | { |
58 | 0 | return realm.heap().allocate<FileReader>(realm, realm); |
59 | 0 | } |
60 | | |
61 | | JS::NonnullGCPtr<FileReader> FileReader::construct_impl(JS::Realm& realm) |
62 | 0 | { |
63 | 0 | return FileReader::create(realm); |
64 | 0 | } |
65 | | |
66 | | // https://w3c.github.io/FileAPI/#blob-package-data |
67 | | WebIDL::ExceptionOr<FileReader::Result> FileReader::blob_package_data(JS::Realm& realm, ByteBuffer bytes, Type type, Optional<String> const& mime_type, Optional<String> const& encoding_name) |
68 | 0 | { |
69 | | // A Blob has an associated package data algorithm, given bytes, a type, a optional mimeType, and a optional encodingName, which switches on type and runs the associated steps: |
70 | 0 | switch (type) { |
71 | 0 | case Type::DataURL: |
72 | | // Return bytes as a DataURL [RFC2397] subject to the considerations below: |
73 | | // Use mimeType as part of the Data URL if it is available in keeping with the Data URL specification [RFC2397]. |
74 | | // If mimeType is not available return a Data URL without a media-type. [RFC2397]. |
75 | 0 | return MUST(URL::create_with_data(mime_type.value_or(String {}), MUST(encode_base64(bytes)), true).to_string()); |
76 | 0 | case Type::Text: { |
77 | | // 1. Let encoding be failure. |
78 | 0 | Optional<StringView> encoding; |
79 | | |
80 | | // 2. If the encodingName is present, set encoding to the result of getting an encoding from encodingName. |
81 | 0 | if (encoding_name.has_value()) |
82 | 0 | encoding = TextCodec::get_standardized_encoding(encoding_name.value()); |
83 | | |
84 | | // 3. If encoding is failure, and mimeType is present: |
85 | 0 | if (!encoding.has_value() && mime_type.has_value()) { |
86 | | // 1. Let type be the result of parse a MIME type given mimeType. |
87 | 0 | auto maybe_type = MimeSniff::MimeType::parse(mime_type.value()); |
88 | | |
89 | | // 2. If type is not failure, set encoding to the result of getting an encoding from type’s parameters["charset"]. |
90 | 0 | if (maybe_type.has_value()) { |
91 | 0 | auto const& type = maybe_type.value(); |
92 | 0 | auto it = type.parameters().find("charset"sv); |
93 | 0 | if (it != type.parameters().end()) |
94 | 0 | encoding = TextCodec::get_standardized_encoding(it->value); |
95 | 0 | } |
96 | 0 | } |
97 | | |
98 | | // 4. If encoding is failure, then set encoding to UTF-8. |
99 | | // 5. Decode bytes using fallback encoding encoding, and return the result. |
100 | 0 | auto decoder = TextCodec::decoder_for(encoding.value_or("UTF-8"sv)); |
101 | 0 | VERIFY(decoder.has_value()); |
102 | 0 | return TRY_OR_THROW_OOM(realm.vm(), convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(decoder.value(), bytes)); |
103 | 0 | } |
104 | 0 | case Type::ArrayBuffer: |
105 | | // Return a new ArrayBuffer whose contents are bytes. |
106 | 0 | return JS::ArrayBuffer::create(realm, move(bytes)); |
107 | 0 | case Type::BinaryString: |
108 | | // FIXME: Return bytes as a binary string, in which every byte is represented by a code unit of equal value [0..255]. |
109 | 0 | return WebIDL::NotSupportedError::create(realm, "BinaryString not supported yet"_string); |
110 | 0 | } |
111 | 0 | VERIFY_NOT_REACHED(); |
112 | 0 | } |
113 | | |
114 | | // https://w3c.github.io/FileAPI/#readOperation |
115 | | WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Optional<String> const& encoding_name) |
116 | 0 | { |
117 | 0 | auto& realm = this->realm(); |
118 | 0 | auto const blobs_type = blob.type(); |
119 | | |
120 | | // 1. If fr’s state is "loading", throw an InvalidStateError DOMException. |
121 | 0 | if (m_state == State::Loading) |
122 | 0 | return WebIDL::InvalidStateError::create(realm, "Read already in progress"_string); |
123 | | |
124 | | // 2. Set fr’s state to "loading". |
125 | 0 | m_state = State::Loading; |
126 | | |
127 | | // 3. Set fr’s result to null. |
128 | 0 | m_result = {}; |
129 | | |
130 | | // 4. Set fr’s error to null. |
131 | 0 | m_error = {}; |
132 | | |
133 | | // 5. Let stream be the result of calling get stream on blob. |
134 | 0 | auto stream = blob.get_stream(); |
135 | | |
136 | | // 6. Let reader be the result of getting a reader from stream. |
137 | 0 | auto reader = TRY(acquire_readable_stream_default_reader(*stream)); |
138 | | |
139 | | // 7. Let bytes be an empty byte sequence. |
140 | 0 | ByteBuffer bytes; |
141 | | |
142 | | // 8. Let chunkPromise be the result of reading a chunk from stream with reader. |
143 | 0 | auto chunk_promise = reader->read(); |
144 | | |
145 | | // 9. Let isFirstChunk be true. |
146 | 0 | bool is_first_chunk = true; |
147 | | |
148 | | // 10. In parallel, while true: |
149 | 0 | Platform::EventLoopPlugin::the().deferred_invoke([this, chunk_promise, reader, bytes, is_first_chunk, &realm, type, encoding_name, blobs_type]() mutable { |
150 | 0 | HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) }; |
151 | 0 | Optional<MonotonicTime> progress_timer; |
152 | |
|
153 | 0 | while (true) { |
154 | 0 | auto& vm = realm.vm(); |
155 | | |
156 | | // 1. Wait for chunkPromise to be fulfilled or rejected. |
157 | 0 | Platform::EventLoopPlugin::the().spin_until([&]() { |
158 | 0 | return chunk_promise->state() == JS::Promise::State::Fulfilled || chunk_promise->state() == JS::Promise::State::Rejected; |
159 | 0 | }); |
160 | | |
161 | | // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr. |
162 | | // NOTE: ISSUE 2 We might change loadstart to be dispatched synchronously, to align with XMLHttpRequest behavior. [Issue #119] |
163 | 0 | if (chunk_promise->state() == JS::Promise::State::Fulfilled && is_first_chunk) { |
164 | 0 | HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), JS::create_heap_function(heap(), [this, &realm]() { |
165 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadstart)); |
166 | 0 | })); |
167 | 0 | } |
168 | | |
169 | | // 3. Set isFirstChunk to false. |
170 | 0 | is_first_chunk = false; |
171 | |
|
172 | 0 | VERIFY(chunk_promise->result().is_object()); |
173 | 0 | auto& result = chunk_promise->result().as_object(); |
174 | |
|
175 | 0 | auto value = MUST(result.get(vm.names.value)); |
176 | 0 | auto done = MUST(result.get(vm.names.done)); |
177 | | |
178 | | // 4. If chunkPromise is fulfilled with an object whose done property is false and whose value property is a Uint8Array object, run these steps: |
179 | 0 | if (chunk_promise->state() == JS::Promise::State::Fulfilled && !done.as_bool() && is<JS::Uint8Array>(value.as_object())) { |
180 | | // 1. Let bs be the byte sequence represented by the Uint8Array object. |
181 | 0 | auto const& byte_sequence = verify_cast<JS::Uint8Array>(value.as_object()); |
182 | | |
183 | | // 2. Append bs to bytes. |
184 | 0 | bytes.append(byte_sequence.data()); |
185 | | |
186 | | // 3. If roughly 50ms have passed since these steps were last invoked, queue a task to fire a progress event called progress at fr. |
187 | 0 | auto now = MonotonicTime::now(); |
188 | 0 | bool enough_time_passed = !progress_timer.has_value() || (now - progress_timer.value() >= AK::Duration::from_milliseconds(50)); |
189 | | // WPT tests for this and expects no progress event to fire when there isn't any data. |
190 | | // See http://wpt.live/FileAPI/reading-data-section/filereader_events.any.html |
191 | 0 | bool contained_data = byte_sequence.array_length().length() > 0; |
192 | 0 | if (enough_time_passed && contained_data) { |
193 | 0 | HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), JS::create_heap_function(heap(), [this, &realm]() { |
194 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::progress)); |
195 | 0 | })); |
196 | 0 | progress_timer = now; |
197 | 0 | } |
198 | | |
199 | | // 4. Set chunkPromise to the result of reading a chunk from stream with reader. |
200 | 0 | chunk_promise = reader->read(); |
201 | 0 | } |
202 | | // 5. Otherwise, if chunkPromise is fulfilled with an object whose done property is true, queue a task to run the following steps and abort this algorithm: |
203 | 0 | else if (chunk_promise->state() == JS::Promise::State::Fulfilled && done.as_bool()) { |
204 | 0 | HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), JS::create_heap_function(heap(), [this, bytes, type, &realm, encoding_name, blobs_type]() { |
205 | | // 1. Set fr’s state to "done". |
206 | 0 | m_state = State::Done; |
207 | | |
208 | | // 2. Let result be the result of package data given bytes, type, blob’s type, and encodingName. |
209 | 0 | auto result = blob_package_data(realm, bytes, type, blobs_type, encoding_name); |
210 | | |
211 | | // 3. If package data threw an exception error: |
212 | 0 | if (result.is_error()) { |
213 | | // FIXME: 1. Set fr’s error to error. |
214 | | |
215 | | // 2. Fire a progress event called error at fr. |
216 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::error)); |
217 | 0 | } |
218 | | // 4. Else: |
219 | 0 | else { |
220 | | // 1. Set fr’s result to result. |
221 | 0 | m_result = result.release_value(); |
222 | | |
223 | | // 2. Fire a progress event called load at the fr. |
224 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::load)); |
225 | 0 | } |
226 | | |
227 | | // 5. If fr’s state is not "loading", fire a progress event called loadend at the fr. |
228 | 0 | if (m_state != State::Loading) |
229 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend)); |
230 | | |
231 | | // NOTE: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired. |
232 | 0 | })); |
233 | |
|
234 | 0 | return; |
235 | 0 | } |
236 | | // 6. Otherwise, if chunkPromise is rejected with an error error, queue a task to run the following steps and abort this algorithm: |
237 | 0 | else if (chunk_promise->state() == JS::Promise::State::Rejected) { |
238 | 0 | HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), JS::create_heap_function(heap(), [this, &realm]() { |
239 | | // 1. Set fr’s state to "done". |
240 | 0 | m_state = State::Done; |
241 | | |
242 | | // FIXME: 2. Set fr’s error to error. |
243 | | |
244 | | // 5. Fire a progress event called error at fr. |
245 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend)); |
246 | | |
247 | | // 4. If fr’s state is not "loading", fire a progress event called loadend at fr. |
248 | 0 | if (m_state != State::Loading) |
249 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend)); |
250 | | |
251 | | // 5. Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired. |
252 | 0 | })); |
253 | 0 | } |
254 | 0 | } |
255 | 0 | }); |
256 | |
|
257 | 0 | return {}; |
258 | 0 | } |
259 | | |
260 | | // https://w3c.github.io/FileAPI/#dfn-readAsDataURL |
261 | | WebIDL::ExceptionOr<void> FileReader::read_as_data_url(Blob& blob) |
262 | 0 | { |
263 | | // The readAsDataURL(blob) method, when invoked, must initiate a read operation for blob with DataURL. |
264 | 0 | return read_operation(blob, Type::DataURL); |
265 | 0 | } |
266 | | |
267 | | // https://w3c.github.io/FileAPI/#dfn-readAsText |
268 | | WebIDL::ExceptionOr<void> FileReader::read_as_text(Blob& blob, Optional<String> const& encoding) |
269 | 0 | { |
270 | | // The readAsText(blob, encoding) method, when invoked, must initiate a read operation for blob with Text and encoding. |
271 | 0 | return read_operation(blob, Type::Text, encoding); |
272 | 0 | } |
273 | | |
274 | | // https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer |
275 | | WebIDL::ExceptionOr<void> FileReader::read_as_array_buffer(Blob& blob) |
276 | 0 | { |
277 | | // The readAsArrayBuffer(blob) method, when invoked, must initiate a read operation for blob with ArrayBuffer. |
278 | 0 | return read_operation(blob, Type::ArrayBuffer); |
279 | 0 | } |
280 | | |
281 | | // https://w3c.github.io/FileAPI/#dfn-readAsBinaryString |
282 | | WebIDL::ExceptionOr<void> FileReader::read_as_binary_string(Blob& blob) |
283 | 0 | { |
284 | | // The readAsBinaryString(blob) method, when invoked, must initiate a read operation for blob with BinaryString. |
285 | | // NOTE: The use of readAsArrayBuffer() is preferred over readAsBinaryString(), which is provided for backwards compatibility. |
286 | 0 | return read_operation(blob, Type::BinaryString); |
287 | 0 | } |
288 | | |
289 | | // https://w3c.github.io/FileAPI/#dfn-abort |
290 | | void FileReader::abort() |
291 | 0 | { |
292 | 0 | auto& realm = this->realm(); |
293 | | |
294 | | // 1. If this's state is "empty" or if this's state is "done" set this's result to null and terminate this algorithm. |
295 | 0 | if (m_state == State::Empty || m_state == State::Done) { |
296 | 0 | m_result = {}; |
297 | 0 | return; |
298 | 0 | } |
299 | | |
300 | | // 2. If this's state is "loading" set this's state to "done" and set this's result to null. |
301 | 0 | if (m_state == State::Loading) { |
302 | 0 | m_state = State::Done; |
303 | 0 | m_result = {}; |
304 | 0 | } |
305 | | |
306 | | // FIXME: 3. If there are any tasks from this on the file reading task source in an affiliated task queue, then remove those tasks from that task queue. |
307 | | |
308 | | // FIXME: 4. Terminate the algorithm for the read method being processed. |
309 | | |
310 | | // 5. Fire a progress event called abort at this. |
311 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::abort)); |
312 | | |
313 | | // 6. If this's state is not "loading", fire a progress event called loadend at this. |
314 | 0 | if (m_state != State::Loading) |
315 | 0 | dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend)); |
316 | 0 | } |
317 | | |
318 | | void FileReader::set_onloadstart(WebIDL::CallbackType* value) |
319 | 0 | { |
320 | 0 | set_event_handler_attribute(HTML::EventNames::loadstart, value); |
321 | 0 | } |
322 | | |
323 | | WebIDL::CallbackType* FileReader::onloadstart() |
324 | 0 | { |
325 | 0 | return event_handler_attribute(HTML::EventNames::loadstart); |
326 | 0 | } |
327 | | |
328 | | void FileReader::set_onprogress(WebIDL::CallbackType* value) |
329 | 0 | { |
330 | 0 | set_event_handler_attribute(HTML::EventNames::progress, value); |
331 | 0 | } |
332 | | |
333 | | WebIDL::CallbackType* FileReader::onprogress() |
334 | 0 | { |
335 | 0 | return event_handler_attribute(HTML::EventNames::progress); |
336 | 0 | } |
337 | | |
338 | | void FileReader::set_onload(WebIDL::CallbackType* value) |
339 | 0 | { |
340 | 0 | set_event_handler_attribute(HTML::EventNames::load, value); |
341 | 0 | } |
342 | | |
343 | | WebIDL::CallbackType* FileReader::onload() |
344 | 0 | { |
345 | 0 | return event_handler_attribute(HTML::EventNames::load); |
346 | 0 | } |
347 | | |
348 | | void FileReader::set_onabort(WebIDL::CallbackType* value) |
349 | 0 | { |
350 | 0 | set_event_handler_attribute(HTML::EventNames::abort, value); |
351 | 0 | } |
352 | | |
353 | | WebIDL::CallbackType* FileReader::onabort() |
354 | 0 | { |
355 | 0 | return event_handler_attribute(HTML::EventNames::abort); |
356 | 0 | } |
357 | | |
358 | | void FileReader::set_onerror(WebIDL::CallbackType* value) |
359 | 0 | { |
360 | 0 | set_event_handler_attribute(HTML::EventNames::error, value); |
361 | 0 | } |
362 | | |
363 | | WebIDL::CallbackType* FileReader::onerror() |
364 | 0 | { |
365 | 0 | return event_handler_attribute(HTML::EventNames::error); |
366 | 0 | } |
367 | | |
368 | | void FileReader::set_onloadend(WebIDL::CallbackType* value) |
369 | 0 | { |
370 | 0 | set_event_handler_attribute(HTML::EventNames::loadend, value); |
371 | 0 | } |
372 | | |
373 | | WebIDL::CallbackType* FileReader::onloadend() |
374 | 0 | { |
375 | 0 | return event_handler_attribute(HTML::EventNames::loadend); |
376 | 0 | } |
377 | | |
378 | | } |