Serenity Operating System
at master 291 lines 13 kB view raw
1/* 2 * Copyright (c) 2022-2023, Kenneth Myhra <kennethmyhra@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/GenericLexer.h> 8#include <LibJS/Runtime/ArrayBuffer.h> 9#include <LibJS/Runtime/Completion.h> 10#include <LibWeb/Bindings/BlobPrototype.h> 11#include <LibWeb/Bindings/ExceptionOrUtils.h> 12#include <LibWeb/Bindings/Intrinsics.h> 13#include <LibWeb/FileAPI/Blob.h> 14#include <LibWeb/Infra/Strings.h> 15#include <LibWeb/WebIDL/AbstractOperations.h> 16 17namespace Web::FileAPI { 18 19WebIDL::ExceptionOr<JS::NonnullGCPtr<Blob>> Blob::create(JS::Realm& realm, ByteBuffer byte_buffer, String type) 20{ 21 return MUST_OR_THROW_OOM(realm.heap().allocate<Blob>(realm, realm, move(byte_buffer), move(type))); 22} 23 24// https://w3c.github.io/FileAPI/#convert-line-endings-to-native 25ErrorOr<String> convert_line_endings_to_native(StringView string) 26{ 27 // 1. Let native line ending be be the code point U+000A LF. 28 auto native_line_ending = "\n"sv; 29 30 // 2. If the underlying platform’s conventions are to represent newlines as a carriage return and line feed sequence, set native line ending to the code point U+000D CR followed by the code point U+000A LF. 31 // NOTE: this step is a no-op since LibWeb does not compile on Windows, which is the only platform we know of that that uses a carriage return and line feed sequence for line endings. 32 33 // 3. Set result to the empty string. 34 StringBuilder result; 35 36 // 4. Let position be a position variable for s, initially pointing at the start of s. 37 auto lexer = GenericLexer { string }; 38 39 // 5. Let token be the result of collecting a sequence of code points that are not equal to U+000A LF or U+000D CR from s given position. 40 // 6. Append token to result. 41 TRY(result.try_append(lexer.consume_until(is_any_of("\n\r"sv)))); 42 43 // 7. While position is not past the end of s: 44 while (!lexer.is_eof()) { 45 // 1. If the code point at position within s equals U+000D CR: 46 if (lexer.peek() == '\r') { 47 // 1. Append native line ending to result. 48 TRY(result.try_append(native_line_ending)); 49 50 // 2. Advance position by 1. 51 lexer.ignore(1); 52 53 // 3. If position is not past the end of s and the code point at position within s equals U+000A LF advance position by 1. 54 if (!lexer.is_eof() && lexer.peek() == '\n') 55 lexer.ignore(1); 56 } 57 // 2. Otherwise if the code point at position within s equals U+000A LF, advance position by 1 and append native line ending to result. 58 else if (lexer.peek() == '\n') { 59 lexer.ignore(1); 60 TRY(result.try_append(native_line_ending)); 61 } 62 63 // 3. Let token be the result of collecting a sequence of code points that are not equal to U+000A LF or U+000D CR from s given position. 64 // 4. Append token to result. 65 TRY(result.try_append(lexer.consume_until(is_any_of("\n\r"sv)))); 66 } 67 // 5. Return result. 68 return result.to_string(); 69} 70 71// https://w3c.github.io/FileAPI/#process-blob-parts 72ErrorOr<ByteBuffer> process_blob_parts(Vector<BlobPart> const& blob_parts, Optional<BlobPropertyBag> const& options) 73{ 74 // 1. Let bytes be an empty sequence of bytes. 75 ByteBuffer bytes {}; 76 77 // 2. For each element in parts: 78 for (auto const& blob_part : blob_parts) { 79 TRY(blob_part.visit( 80 // 1. If element is a USVString, run the following sub-steps: 81 [&](String const& string) -> ErrorOr<void> { 82 // 1. Let s be element. 83 auto s = string; 84 85 // 2. If the endings member of options is "native", set s to the result of converting line endings to native of element. 86 if (options.has_value() && options->endings == Bindings::EndingType::Native) 87 s = TRY(convert_line_endings_to_native(s)); 88 89 // NOTE: The AK::String is always UTF-8. 90 // 3. Append the result of UTF-8 encoding s to bytes. 91 return bytes.try_append(s.bytes()); 92 }, 93 // 2. If element is a BufferSource, get a copy of the bytes held by the buffer source, and append those bytes to bytes. 94 [&](JS::Handle<JS::Object> const& buffer_source) -> ErrorOr<void> { 95 auto data_buffer = TRY(WebIDL::get_buffer_source_copy(*buffer_source.cell())); 96 return bytes.try_append(data_buffer.bytes()); 97 }, 98 // 3. If element is a Blob, append the bytes it represents to bytes. 99 [&](JS::Handle<Blob> const& blob) -> ErrorOr<void> { 100 return bytes.try_append(blob->bytes()); 101 })); 102 } 103 // 3. Return bytes. 104 return bytes; 105} 106 107bool is_basic_latin(StringView view) 108{ 109 for (auto code_point : view) { 110 if (code_point < 0x0020 || code_point > 0x007E) 111 return false; 112 } 113 return true; 114} 115 116Blob::Blob(JS::Realm& realm) 117 : PlatformObject(realm) 118{ 119} 120 121Blob::Blob(JS::Realm& realm, ByteBuffer byte_buffer, String type) 122 : PlatformObject(realm) 123 , m_byte_buffer(move(byte_buffer)) 124 , m_type(move(type)) 125{ 126} 127 128Blob::Blob(JS::Realm& realm, ByteBuffer byte_buffer) 129 : PlatformObject(realm) 130 , m_byte_buffer(move(byte_buffer)) 131{ 132} 133 134Blob::~Blob() = default; 135 136JS::ThrowCompletionOr<void> Blob::initialize(JS::Realm& realm) 137{ 138 MUST_OR_THROW_OOM(Base::initialize(realm)); 139 set_prototype(&Bindings::ensure_web_prototype<Bindings::BlobPrototype>(realm, "Blob")); 140 141 return {}; 142} 143 144// https://w3c.github.io/FileAPI/#ref-for-dom-blob-blob 145WebIDL::ExceptionOr<JS::NonnullGCPtr<Blob>> Blob::create(JS::Realm& realm, Optional<Vector<BlobPart>> const& blob_parts, Optional<BlobPropertyBag> const& options) 146{ 147 auto& vm = realm.vm(); 148 149 // 1. If invoked with zero parameters, return a new Blob object consisting of 0 bytes, with size set to 0, and with type set to the empty string. 150 if (!blob_parts.has_value() && !options.has_value()) 151 return MUST_OR_THROW_OOM(realm.heap().allocate<Blob>(realm, realm)); 152 153 ByteBuffer byte_buffer {}; 154 // 2. Let bytes be the result of processing blob parts given blobParts and options. 155 if (blob_parts.has_value()) { 156 byte_buffer = TRY_OR_THROW_OOM(realm.vm(), process_blob_parts(blob_parts.value(), options)); 157 } 158 159 auto type = String {}; 160 // 3. If the type member of the options argument is not the empty string, run the following sub-steps: 161 if (options.has_value() && !options->type.is_empty()) { 162 // 1. If the type member is provided and is not the empty string, let t be set to the type dictionary member. 163 // If t contains any characters outside the range U+0020 to U+007E, then set t to the empty string and return from these substeps. 164 // NOTE: t is set to empty string at declaration. 165 if (!options->type.is_empty()) { 166 if (is_basic_latin(options->type)) 167 type = options->type; 168 } 169 170 // 2. Convert every character in t to ASCII lowercase. 171 if (!type.is_empty()) 172 type = TRY_OR_THROW_OOM(vm, Infra::to_ascii_lowercase(type)); 173 } 174 175 // 4. Return a Blob object referring to bytes as its associated byte sequence, with its size set to the length of bytes, and its type set to the value of t from the substeps above. 176 return MUST_OR_THROW_OOM(realm.heap().allocate<Blob>(realm, realm, move(byte_buffer), move(type))); 177} 178 179WebIDL::ExceptionOr<JS::NonnullGCPtr<Blob>> Blob::construct_impl(JS::Realm& realm, Optional<Vector<BlobPart>> const& blob_parts, Optional<BlobPropertyBag> const& options) 180{ 181 return Blob::create(realm, blob_parts, options); 182} 183 184// https://w3c.github.io/FileAPI/#dfn-slice 185WebIDL::ExceptionOr<JS::NonnullGCPtr<Blob>> Blob::slice(Optional<i64> start, Optional<i64> end, Optional<String> const& content_type) 186{ 187 auto& vm = realm().vm(); 188 189 // 1. The optional start parameter is a value for the start point of a slice() call, and must be treated as a byte-order position, with the zeroth position representing the first byte. 190 // User agents must process slice() with start normalized according to the following: 191 i64 relative_start; 192 if (!start.has_value()) { 193 // a. If the optional start parameter is not used as a parameter when making this call, let relativeStart be 0. 194 relative_start = 0; 195 } else { 196 auto start_value = start.value(); 197 // b. If start is negative, let relativeStart be max((size + start), 0). 198 if (start_value < 0) { 199 relative_start = max((size() + start_value), 0); 200 } 201 // c. Else, let relativeStart be min(start, size). 202 else { 203 relative_start = min(start_value, size()); 204 } 205 } 206 207 // 2. The optional end parameter is a value for the end point of a slice() call. User agents must process slice() with end normalized according to the following: 208 i64 relative_end; 209 if (!end.has_value()) { 210 // a. If the optional end parameter is not used as a parameter when making this call, let relativeEnd be size. 211 relative_end = size(); 212 } else { 213 auto end_value = end.value(); 214 // b. If end is negative, let relativeEnd be max((size + end), 0). 215 if (end_value < 0) { 216 relative_end = max((size() + end_value), 0); 217 } 218 // c Else, let relativeEnd be min(end, size). 219 else { 220 relative_end = min(end_value, size()); 221 } 222 } 223 224 // 3. The optional contentType parameter is used to set the ASCII-encoded string in lower case representing the media type of the Blob. 225 // User agents must process the slice() with contentType normalized according to the following: 226 String relative_content_type; 227 if (!content_type.has_value()) { 228 // a. If the contentType parameter is not provided, let relativeContentType be set to the empty string. 229 relative_content_type = String {}; 230 } else { 231 // b. Else let relativeContentType be set to contentType and run the substeps below: 232 233 // FIXME: 1. If relativeContentType contains any characters outside the range of U+0020 to U+007E, then set relativeContentType to the empty string and return from these substeps. 234 235 // 2. Convert every character in relativeContentType to ASCII lowercase. 236 relative_content_type = TRY_OR_THROW_OOM(vm, Infra::to_ascii_lowercase(content_type.value())); 237 } 238 239 // 4. Let span be max((relativeEnd - relativeStart), 0). 240 auto span = max((relative_end - relative_start), 0); 241 242 // 5. Return a new Blob object S with the following characteristics: 243 // a. S refers to span consecutive bytes from this, beginning with the byte at byte-order position relativeStart. 244 // b. S.size = span. 245 // c. S.type = relativeContentType. 246 auto byte_buffer = TRY_OR_THROW_OOM(vm, m_byte_buffer.slice(relative_start, span)); 247 return MUST_OR_THROW_OOM(heap().allocate<Blob>(realm(), realm(), move(byte_buffer), move(relative_content_type))); 248} 249 250// https://w3c.github.io/FileAPI/#dom-blob-text 251WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> Blob::text() 252{ 253 auto& realm = this->realm(); 254 auto& vm = realm.vm(); 255 256 // FIXME: 1. Let stream be the result of calling get stream on this. 257 // FIXME: 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception. 258 259 // FIXME: We still need to implement ReadableStream for this step to be fully valid. 260 // 3. Let promise be the result of reading all bytes from stream with reader 261 auto promise = JS::Promise::create(realm); 262 auto result = TRY(Bindings::throw_dom_exception_if_needed(vm, [&]() { return JS::PrimitiveString::create(vm, StringView { m_byte_buffer.bytes() }); })); 263 264 // 4. Return the result of transforming promise by a fulfillment handler that returns the result of running UTF-8 decode on its first argument. 265 promise->fulfill(result); 266 return promise; 267} 268 269// https://w3c.github.io/FileAPI/#dom-blob-arraybuffer 270JS::Promise* Blob::array_buffer() 271{ 272 // FIXME: 1. Let stream be the result of calling get stream on this. 273 // FIXME: 2. Let reader be the result of getting a reader from stream. If that threw an exception, return a new promise rejected with that exception. 274 275 // FIXME: We still need to implement ReadableStream for this step to be fully valid. 276 // 3. Let promise be the result of reading all bytes from stream with reader. 277 auto promise = JS::Promise::create(realm()); 278 auto buffer_result = JS::ArrayBuffer::create(realm(), m_byte_buffer.size()); 279 if (buffer_result.is_error()) { 280 promise->reject(buffer_result.release_error().value().release_value()); 281 return promise; 282 } 283 auto buffer = buffer_result.release_value(); 284 buffer->buffer().overwrite(0, m_byte_buffer.data(), m_byte_buffer.size()); 285 286 // 4. Return the result of transforming promise by a fulfillment handler that returns a new ArrayBuffer whose contents are its first argument. 287 promise->fulfill(buffer); 288 return promise; 289} 290 291}