2023-09-09 13:29:01 +12:00
/*
2025-01-05 10:56:23 +13:00
* Copyright ( c ) 2023 - 2025 , Shannon Booth < shannon @ serenityos . org >
2023-09-09 13:29:01 +12:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/Assertions.h>
# include <AK/Base64.h>
# include <AK/ByteBuffer.h>
2024-10-15 06:21:38 +02:00
# include <AK/Time.h>
2024-11-15 04:01:23 +13:00
# include <LibGC/Heap.h>
2023-09-09 13:29:01 +12:00
# include <LibJS/Runtime/Promise.h>
# include <LibJS/Runtime/Realm.h>
# include <LibJS/Runtime/TypedArray.h>
# include <LibTextCodec/Decoder.h>
2024-04-27 12:09:58 +12:00
# include <LibWeb/Bindings/FileReaderPrototype.h>
2023-09-09 13:29:01 +12:00
# include <LibWeb/Bindings/Intrinsics.h>
# include <LibWeb/DOM/Event.h>
# include <LibWeb/DOM/EventTarget.h>
# include <LibWeb/FileAPI/Blob.h>
# include <LibWeb/FileAPI/FileReader.h>
# include <LibWeb/HTML/EventLoop/EventLoop.h>
# include <LibWeb/HTML/EventNames.h>
2025-01-05 11:05:53 +13:00
# include <LibWeb/HTML/Scripting/Agent.h>
2023-09-09 13:29:01 +12:00
# include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
# include <LibWeb/MimeSniff/MimeType.h>
# include <LibWeb/Platform/EventLoopPlugin.h>
# include <LibWeb/Streams/ReadableStream.h>
# include <LibWeb/Streams/ReadableStreamDefaultReader.h>
# include <LibWeb/WebIDL/DOMException.h>
# include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web : : FileAPI {
2024-11-15 04:01:23 +13:00
GC_DEFINE_ALLOCATOR ( FileReader ) ;
2023-11-19 19:47:52 +01:00
2023-09-09 13:29:01 +12:00
FileReader : : ~ FileReader ( ) = default ;
FileReader : : FileReader ( JS : : Realm & realm )
: DOM : : EventTarget ( realm )
{
}
void FileReader : : initialize ( JS : : Realm & realm )
{
Base : : initialize ( realm ) ;
2024-03-16 13:13:08 +01:00
WEB_SET_PROTOTYPE_FOR_INTERFACE ( FileReader ) ;
2023-09-09 13:29:01 +12:00
}
void FileReader : : visit_edges ( JS : : Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
visitor . visit ( m_error ) ;
}
2024-11-15 04:01:23 +13:00
GC : : Ref < FileReader > FileReader : : create ( JS : : Realm & realm )
2023-09-09 13:29:01 +12:00
{
2024-11-14 05:50:17 +13:00
return realm . create < FileReader > ( realm ) ;
2023-09-09 13:29:01 +12:00
}
2024-11-15 04:01:23 +13:00
GC : : Ref < FileReader > FileReader : : construct_impl ( JS : : Realm & realm )
2023-09-09 13:29:01 +12:00
{
return FileReader : : create ( realm ) ;
}
// https://w3c.github.io/FileAPI/#blob-package-data
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 )
{
// 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:
switch ( type ) {
case Type : : DataURL :
// Return bytes as a DataURL [RFC2397] subject to the considerations below:
// Use mimeType as part of the Data URL if it is available in keeping with the Data URL specification [RFC2397].
// If mimeType is not available return a Data URL without a media-type. [RFC2397].
2024-12-03 22:31:33 +13:00
return URL : : create_with_data ( mime_type . value_or ( String { } ) , MUST ( encode_base64 ( bytes ) ) , true ) . to_string ( ) ;
2023-09-09 13:29:01 +12:00
case Type : : Text : {
// 1. Let encoding be failure.
Optional < StringView > encoding ;
// 2. If the encodingName is present, set encoding to the result of getting an encoding from encodingName.
if ( encoding_name . has_value ( ) )
encoding = TextCodec : : get_standardized_encoding ( encoding_name . value ( ) ) ;
// 3. If encoding is failure, and mimeType is present:
if ( ! encoding . has_value ( ) & & mime_type . has_value ( ) ) {
// 1. Let type be the result of parse a MIME type given mimeType.
auto maybe_type = MimeSniff : : MimeType : : parse ( mime_type . value ( ) ) ;
// 2. If type is not failure, set encoding to the result of getting an encoding from type’ s parameters["charset"].
2024-10-14 11:18:44 +02:00
if ( maybe_type . has_value ( ) ) {
auto const & type = maybe_type . value ( ) ;
2023-09-09 13:29:01 +12:00
auto it = type . parameters ( ) . find ( " charset " sv ) ;
if ( it ! = type . parameters ( ) . end ( ) )
encoding = TextCodec : : get_standardized_encoding ( it - > value ) ;
}
}
// 4. If encoding is failure, then set encoding to UTF-8.
// 5. Decode bytes using fallback encoding encoding, and return the result.
auto decoder = TextCodec : : decoder_for ( encoding . value_or ( " UTF-8 " sv ) ) ;
VERIFY ( decoder . has_value ( ) ) ;
return TRY_OR_THROW_OOM ( realm . vm ( ) , convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark ( decoder . value ( ) , bytes ) ) ;
}
case Type : : ArrayBuffer :
// Return a new ArrayBuffer whose contents are bytes.
return JS : : ArrayBuffer : : create ( realm , move ( bytes ) ) ;
case Type : : BinaryString :
2025-01-24 00:32:30 +13:00
// Return bytes as a binary string, in which every byte is represented by a code unit of equal value [0..255].
Vector < u16 > builder ;
builder . ensure_capacity ( bytes . size ( ) ) ;
for ( auto byte : bytes . bytes ( ) )
builder . unchecked_append ( byte ) ;
return MUST ( Utf16View { builder } . to_utf8 ( Utf16View : : AllowInvalidCodeUnits : : Yes ) ) ;
2023-09-09 13:29:01 +12:00
}
VERIFY_NOT_REACHED ( ) ;
}
2025-01-05 11:05:53 +13:00
void FileReader : : queue_a_task ( GC : : Ref < GC : : Function < void ( ) > > task )
{
// To implement the requirement of removing queued tasks on an abort we keep track of a list of
// task IDs which are pending evaluation. This allows an abort to go through the task queue to
// remove those pending tasks.
auto wrapper_task = GC : : create_function ( heap ( ) , [ this , task ] {
auto & event_loop = * HTML : : relevant_agent ( * this ) . event_loop ;
VERIFY ( event_loop . currently_running_task ( ) ) ;
auto & current_task = * event_loop . currently_running_task ( ) ;
task - > function ( ) ( ) ;
m_pending_tasks . remove ( current_task . id ( ) ) ;
} ) ;
auto id = HTML : : queue_global_task ( HTML : : Task : : Source : : FileReading , realm ( ) . global_object ( ) , wrapper_task ) ;
m_pending_tasks . set ( id ) ;
}
2023-09-09 13:29:01 +12:00
// https://w3c.github.io/FileAPI/#readOperation
WebIDL : : ExceptionOr < void > FileReader : : read_operation ( Blob & blob , Type type , Optional < String > const & encoding_name )
{
auto & realm = this - > realm ( ) ;
auto const blobs_type = blob . type ( ) ;
// 1. If fr’ s state is "loading", throw an InvalidStateError DOMException.
if ( m_state = = State : : Loading )
2024-10-12 20:56:21 +02:00
return WebIDL : : InvalidStateError : : create ( realm , " Read already in progress " _string ) ;
2023-09-09 13:29:01 +12:00
// 2. Set fr’ s state to "loading".
m_state = State : : Loading ;
2025-01-05 11:05:53 +13:00
m_is_aborted = false ;
2023-09-09 13:29:01 +12:00
// 3. Set fr’ s result to null.
m_result = { } ;
// 4. Set fr’ s error to null.
m_error = { } ;
// 5. Let stream be the result of calling get stream on blob.
2024-04-29 18:01:44 -04:00
auto stream = blob . get_stream ( ) ;
2023-09-09 13:29:01 +12:00
// 6. Let reader be the result of getting a reader from stream.
2024-12-24 12:38:25 +13:00
auto reader = TRY ( stream - > get_a_reader ( ) ) ;
2023-09-09 13:29:01 +12:00
// 7. Let bytes be an empty byte sequence.
ByteBuffer bytes ;
// 8. Let chunkPromise be the result of reading a chunk from stream with reader.
2024-04-29 18:34:19 -04:00
auto chunk_promise = reader - > read ( ) ;
2023-09-09 13:29:01 +12:00
// 9. Let isFirstChunk be true.
bool is_first_chunk = true ;
// 10. In parallel, while true:
2024-11-15 04:01:23 +13:00
Platform : : EventLoopPlugin : : the ( ) . deferred_invoke ( GC : : create_function ( heap ( ) , [ this , chunk_promise , reader , bytes , is_first_chunk , & realm , type , encoding_name , blobs_type ] ( ) mutable {
2024-10-24 20:39:18 +13:00
HTML : : TemporaryExecutionContext execution_context { realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes } ;
2024-10-15 06:21:38 +02:00
Optional < MonotonicTime > progress_timer ;
2025-01-05 11:05:53 +13:00
while ( ! m_is_aborted ) {
2023-09-09 13:29:01 +12:00
auto & vm = realm . vm ( ) ;
2024-10-25 12:38:19 -06:00
// FIXME: Try harder to not reach into the [[Promise]] slot of chunkPromise
2025-01-21 09:12:05 -05:00
auto promise = GC : : Ref { as < JS : : Promise > ( * chunk_promise - > promise ( ) ) } ;
2023-09-09 13:29:01 +12:00
// 1. Wait for chunkPromise to be fulfilled or rejected.
2024-10-25 12:38:19 -06:00
// FIXME: Create spec issue to use WebIDL react to promise steps here instead of this custom logic
2025-01-03 12:08:37 +13:00
HTML : : main_thread_event_loop ( ) . spin_until ( GC : : create_function ( heap ( ) , [ promise ] ( ) {
2024-10-25 12:38:19 -06:00
return promise - > state ( ) = = JS : : Promise : : State : : Fulfilled | | promise - > state ( ) = = JS : : Promise : : State : : Rejected ;
2024-10-31 01:06:56 +13:00
} ) ) ;
2023-09-09 13:29:01 +12:00
2025-01-05 11:05:53 +13:00
if ( m_is_aborted )
return ;
2023-09-09 13:29:01 +12:00
// 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr.
// NOTE: ISSUE 2 We might change loadstart to be dispatched synchronously, to align with XMLHttpRequest behavior. [Issue #119]
2024-10-25 12:38:19 -06:00
if ( promise - > state ( ) = = JS : : Promise : : State : : Fulfilled & & is_first_chunk ) {
2025-01-05 11:05:53 +13:00
queue_a_task ( GC : : create_function ( heap ( ) , [ this , & realm ] ( ) {
2023-09-09 13:29:01 +12:00
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : loadstart ) ) ;
2024-04-16 22:04:01 +02:00
} ) ) ;
2023-09-09 13:29:01 +12:00
}
// 3. Set isFirstChunk to false.
is_first_chunk = false ;
2024-10-25 12:38:19 -06:00
VERIFY ( promise - > result ( ) . is_object ( ) ) ;
auto & result = promise - > result ( ) . as_object ( ) ;
2023-09-09 13:29:01 +12:00
auto value = MUST ( result . get ( vm . names . value ) ) ;
auto done = MUST ( result . get ( vm . names . done ) ) ;
// 4. If chunkPromise is fulfilled with an object whose done property is false and whose value property is a Uint8Array object, run these steps:
2024-10-25 12:38:19 -06:00
if ( promise - > state ( ) = = JS : : Promise : : State : : Fulfilled & & ! done . as_bool ( ) & & is < JS : : Uint8Array > ( value . as_object ( ) ) ) {
2023-09-09 13:29:01 +12:00
// 1. Let bs be the byte sequence represented by the Uint8Array object.
2025-01-21 09:12:05 -05:00
auto const & byte_sequence = as < JS : : Uint8Array > ( value . as_object ( ) ) ;
2023-09-09 13:29:01 +12:00
// 2. Append bs to bytes.
bytes . append ( byte_sequence . data ( ) ) ;
2024-10-15 06:21:38 +02:00
// 3. If roughly 50ms have passed since these steps were last invoked, queue a task to fire a progress event called progress at fr.
auto now = MonotonicTime : : now ( ) ;
bool enough_time_passed = ! progress_timer . has_value ( ) | | ( now - progress_timer . value ( ) > = AK : : Duration : : from_milliseconds ( 50 ) ) ;
// WPT tests for this and expects no progress event to fire when there isn't any data.
// See http://wpt.live/FileAPI/reading-data-section/filereader_events.any.html
bool contained_data = byte_sequence . array_length ( ) . length ( ) > 0 ;
if ( enough_time_passed & & contained_data ) {
2025-01-05 11:05:53 +13:00
queue_a_task ( GC : : create_function ( heap ( ) , [ this , & realm ] ( ) {
2024-10-15 06:21:38 +02:00
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : progress ) ) ;
} ) ) ;
progress_timer = now ;
}
2023-09-09 13:29:01 +12:00
// 4. Set chunkPromise to the result of reading a chunk from stream with reader.
2024-04-29 18:34:19 -04:00
chunk_promise = reader - > read ( ) ;
2023-09-09 13:29:01 +12:00
}
// 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:
2024-10-25 12:38:19 -06:00
else if ( promise - > state ( ) = = JS : : Promise : : State : : Fulfilled & & done . as_bool ( ) ) {
2025-01-05 11:05:53 +13:00
queue_a_task ( GC : : create_function ( heap ( ) , [ this , bytes , type , & realm , encoding_name , blobs_type ] ( ) {
2023-09-09 13:29:01 +12:00
// 1. Set fr’ s state to "done".
m_state = State : : Done ;
// 2. Let result be the result of package data given bytes, type, blob’ s type, and encodingName.
auto result = blob_package_data ( realm , bytes , type , blobs_type , encoding_name ) ;
// 3. If package data threw an exception error:
if ( result . is_error ( ) ) {
// FIXME: 1. Set fr’ s error to error.
// 2. Fire a progress event called error at fr.
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : error ) ) ;
}
// 4. Else:
else {
// 1. Set fr’ s result to result.
m_result = result . release_value ( ) ;
// 2. Fire a progress event called load at the fr.
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : load ) ) ;
}
// 5. If fr’ s state is not "loading", fire a progress event called loadend at the fr.
if ( m_state ! = State : : Loading )
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : loadend ) ) ;
2025-01-05 10:56:23 +13:00
// Spec-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.
2024-04-16 22:04:01 +02:00
} ) ) ;
2023-09-09 13:29:01 +12:00
return ;
}
// 6. Otherwise, if chunkPromise is rejected with an error error, queue a task to run the following steps and abort this algorithm:
2024-10-25 12:38:19 -06:00
else if ( promise - > state ( ) = = JS : : Promise : : State : : Rejected ) {
2025-01-05 11:05:53 +13:00
queue_a_task ( GC : : create_function ( heap ( ) , [ this , & realm ] ( ) {
2023-09-09 13:29:01 +12:00
// 1. Set fr’ s state to "done".
m_state = State : : Done ;
// FIXME: 2. Set fr’ s error to error.
2025-01-05 10:56:23 +13:00
// 3. Fire a progress event called error at fr.
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : error ) ) ;
2023-09-09 13:29:01 +12:00
// 4. If fr’ s state is not "loading", fire a progress event called loadend at fr.
if ( m_state ! = State : : Loading )
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : loadend ) ) ;
2025-01-05 10:56:23 +13:00
// Spec-Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired.
2024-04-16 22:04:01 +02:00
} ) ) ;
2024-10-25 12:38:19 -06:00
return ;
2023-09-09 13:29:01 +12:00
}
}
2024-10-31 02:39:29 +13:00
} ) ) ;
2023-09-09 13:29:01 +12:00
return { } ;
}
// https://w3c.github.io/FileAPI/#dfn-readAsDataURL
WebIDL : : ExceptionOr < void > FileReader : : read_as_data_url ( Blob & blob )
{
// The readAsDataURL(blob) method, when invoked, must initiate a read operation for blob with DataURL.
return read_operation ( blob , Type : : DataURL ) ;
}
// https://w3c.github.io/FileAPI/#dfn-readAsText
WebIDL : : ExceptionOr < void > FileReader : : read_as_text ( Blob & blob , Optional < String > const & encoding )
{
// The readAsText(blob, encoding) method, when invoked, must initiate a read operation for blob with Text and encoding.
return read_operation ( blob , Type : : Text , encoding ) ;
}
// https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
WebIDL : : ExceptionOr < void > FileReader : : read_as_array_buffer ( Blob & blob )
{
// The readAsArrayBuffer(blob) method, when invoked, must initiate a read operation for blob with ArrayBuffer.
return read_operation ( blob , Type : : ArrayBuffer ) ;
}
// https://w3c.github.io/FileAPI/#dfn-readAsBinaryString
WebIDL : : ExceptionOr < void > FileReader : : read_as_binary_string ( Blob & blob )
{
// The readAsBinaryString(blob) method, when invoked, must initiate a read operation for blob with BinaryString.
// NOTE: The use of readAsArrayBuffer() is preferred over readAsBinaryString(), which is provided for backwards compatibility.
return read_operation ( blob , Type : : BinaryString ) ;
}
// https://w3c.github.io/FileAPI/#dfn-abort
void FileReader : : abort ( )
{
auto & realm = this - > realm ( ) ;
// 1. If this's state is "empty" or if this's state is "done" set this's result to null and terminate this algorithm.
if ( m_state = = State : : Empty | | m_state = = State : : Done ) {
m_result = { } ;
return ;
}
// 2. If this's state is "loading" set this's state to "done" and set this's result to null.
if ( m_state = = State : : Loading ) {
m_state = State : : Done ;
m_result = { } ;
}
2025-01-05 11:05:53 +13:00
// 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.
auto & event_loop = * HTML : : relevant_agent ( * this ) . event_loop ;
event_loop . task_queue ( ) . remove_tasks_matching ( [ & ] ( auto const & task ) {
return m_pending_tasks . contains ( task . id ( ) ) ;
} ) ;
m_pending_tasks . clear ( ) ;
2023-09-09 13:29:01 +12:00
2025-01-05 11:05:53 +13:00
// 4. Terminate the algorithm for the read method being processed.
m_is_aborted = true ;
2023-09-09 13:29:01 +12:00
// 5. Fire a progress event called abort at this.
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : abort ) ) ;
// 6. If this's state is not "loading", fire a progress event called loadend at this.
if ( m_state ! = State : : Loading )
dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : loadend ) ) ;
}
void FileReader : : set_onloadstart ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : loadstart , value ) ;
}
WebIDL : : CallbackType * FileReader : : onloadstart ( )
{
return event_handler_attribute ( HTML : : EventNames : : loadstart ) ;
}
void FileReader : : set_onprogress ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : progress , value ) ;
}
WebIDL : : CallbackType * FileReader : : onprogress ( )
{
return event_handler_attribute ( HTML : : EventNames : : progress ) ;
}
void FileReader : : set_onload ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : load , value ) ;
}
WebIDL : : CallbackType * FileReader : : onload ( )
{
return event_handler_attribute ( HTML : : EventNames : : load ) ;
}
void FileReader : : set_onabort ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : abort , value ) ;
}
WebIDL : : CallbackType * FileReader : : onabort ( )
{
return event_handler_attribute ( HTML : : EventNames : : abort ) ;
}
void FileReader : : set_onerror ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : error , value ) ;
}
WebIDL : : CallbackType * FileReader : : onerror ( )
{
return event_handler_attribute ( HTML : : EventNames : : error ) ;
}
void FileReader : : set_onloadend ( WebIDL : : CallbackType * value )
{
set_event_handler_attribute ( HTML : : EventNames : : loadend , value ) ;
}
WebIDL : : CallbackType * FileReader : : onloadend ( )
{
return event_handler_attribute ( HTML : : EventNames : : loadend ) ;
}
}