diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 765aa2dd088e84..b018bf72a0440c 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -279,6 +279,119 @@ for (const b of buf) { Additionally, the [`buf.values()`][], [`buf.keys()`][], and [`buf.entries()`][] methods can be used to create iterators. +## Class: `Blob` + + +> Stability: 1 - Experimental + +A [`Blob`][] encapsulates immutable, raw data that can be safely shared across +multiple worker threads. + +### `new buffer.Blob([sources[, options]])` + + +* `sources` {string[]|ArrayBuffer[]|TypedArray[]|DataView[]|Blob[]} An array + of string, {ArrayBuffer}, {TypedArray}, {DataView}, or {Blob} objects, or + any mix of such objects, that will be stored within the `Blob`. +* `options` {Object} + * `encoding` {string} The character encoding to use for string sources. + **Default**: `'utf8'`. + * `type` {string} The Blob content-type. The intent is for `type` to convey + the MIME media type of the data, however no validation of the type format + is performed. + +Creates a new `Blob` object containing a concatenation of the given sources. + +{ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into +the 'Blob' and can therefore be safely modified after the 'Blob' is created. + +String sources are also copied into the `Blob`. + +### `blob.arrayBuffer()` + + +* Returns: {Promise} + +Returns a promise that fulfills with an {ArrayBuffer} containing a copy of +the `Blob` data. + +### `blob.size` + + +The total size of the `Blob` in bytes. + +### `blob.slice([start, [end, [type]]])` + + +* `start` {number} The starting index. +* `end` {number} The ending index. +* `type` {string} The content-type for the new `Blob` + +Creates and returns a new `Blob` containing a subset of this `Blob` objects +data. The original `Blob` is not alterered. + +### `blob.text()` + + +* Returns: {Promise} + +Returns a promise that resolves the contents of the `Blob` decoded as a UTF-8 +string. + +### `blob.type` + + +* Type: {string} + +The content-type of the `Blob`. + +### `Blob` objects and `MessageChannel` + +Once a {Blob} object is created, it can be sent via `MessagePort` to multiple +destinations without transfering or immediately copying the data. The data +contained by the `Blob` is copied only when the `arrayBuffer()` or `text()` +methods are called. + +```js +const { Blob } = require('buffer'); +const blob = new Blob(['hello there']); +const { setTimeout: delay } = require('timers/promises'); + +const mc1 = new MessageChannel(); +const mc2 = new MessageChannel(); + +mc1.port1.onmessage = async ({ data }) => { + console.log(await data.arrayBuffer()); + mc1.port1.close(); +}; + +mc2.port1.onmessage = async ({ data }) => { + await delay(1000); + console.log(await data.arrayBuffer()); + mc2.port1.close(); +}; + +mc1.port2.postMessage(blob); +mc2.port2.postMessage(blob); + +// The Blob is still usable after posting. +data.text().then(console.log); +``` + ## Class: `Buffer` The `Buffer` class is a global type for dealing with binary data directly. @@ -3380,6 +3493,7 @@ introducing security vulnerabilities into an application. [UTF-8]: https://en.wikipedia.org/wiki/UTF-8 [WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/ [`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer +[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob [`Buffer.alloc()`]: #buffer_static_method_buffer_alloc_size_fill_encoding [`Buffer.allocUnsafe()`]: #buffer_static_method_buffer_allocunsafe_size [`Buffer.allocUnsafeSlow()`]: #buffer_static_method_buffer_allocunsafeslow_size diff --git a/lib/buffer.js b/lib/buffer.js index d3034a46c60b29..53047385d26c88 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -115,6 +115,10 @@ const { createUnsafeBuffer } = require('internal/buffer'); +const { + Blob, +} = require('internal/blob'); + FastBuffer.prototype.constructor = Buffer; Buffer.prototype = FastBuffer.prototype; addBufferPrototypeMethods(Buffer.prototype); @@ -1191,6 +1195,7 @@ if (internalBinding('config').hasIntl) { } module.exports = { + Blob, Buffer, SlowBuffer, transcode, diff --git a/lib/internal/blob.js b/lib/internal/blob.js new file mode 100644 index 00000000000000..e49b3710e0768a --- /dev/null +++ b/lib/internal/blob.js @@ -0,0 +1,238 @@ +'use strict'; + +const { + ArrayFrom, + ObjectSetPrototypeOf, + Promise, + PromiseResolve, + RegExpPrototypeTest, + StringPrototypeToLowerCase, + Symbol, + SymbolIterator, + Uint8Array, +} = primordials; + +const { + createBlob, + FixedSizeBlobCopyJob, +} = internalBinding('buffer'); + +const { + JSTransferable, + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); + +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); + +const { + customInspectSymbol: kInspect, + emitExperimentalWarning, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); + +const { + AbortError, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_BUFFER_TOO_LARGE, + ERR_OUT_OF_RANGE, + } +} = require('internal/errors'); + +const { + validateObject, + validateString, + validateUint32, + isUint32, +} = require('internal/validators'); + +const kHandle = Symbol('kHandle'); +const kType = Symbol('kType'); +const kLength = Symbol('kLength'); + +let Buffer; + +function deferred() { + let res, rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return { promise, resolve: res, reject: rej }; +} + +function lazyBuffer() { + if (Buffer === undefined) + Buffer = require('buffer').Buffer; + return Buffer; +} + +function isBlob(object) { + return object?.[kHandle] !== undefined; +} + +function getSource(source, encoding) { + if (isBlob(source)) + return [source.size, source[kHandle]]; + + if (typeof source === 'string') { + source = lazyBuffer().from(source, encoding); + } else if (isAnyArrayBuffer(source)) { + source = new Uint8Array(source); + } else if (!isArrayBufferView(source)) { + throw new ERR_INVALID_ARG_TYPE( + 'source', + [ + 'string', + 'ArrayBuffer', + 'SharedArrayBuffer', + 'Buffer', + 'TypedArray', + 'DataView' + ], + source); + } + + // We copy into a new Uint8Array because the underlying + // BackingStores are going to be detached and owned by + // the Blob. We also don't want to have to worry about + // byte offsets. + source = new Uint8Array(source); + return [source.byteLength, source]; +} + +class InternalBlob extends JSTransferable { + constructor(handle, length, type = '') { + super(); + this[kHandle] = handle; + this[kType] = type; + this[kLength] = length; + } +} + +class Blob extends JSTransferable { + constructor(sources = [], options) { + emitExperimentalWarning('buffer.Blob'); + if (sources === null || + typeof sources[SymbolIterator] !== 'function' || + typeof sources === 'string') { + throw new ERR_INVALID_ARG_TYPE('sources', 'Iterable', sources); + } + if (options !== undefined) + validateObject(options, 'options'); + const { + encoding = 'utf8', + type = '', + } = { ...options }; + + let length = 0; + const sources_ = ArrayFrom(sources, (source) => { + const { 0: len, 1: src } = getSource(source, encoding); + length += len; + return src; + }); + + // This is a MIME media type but we're not actively checking the syntax. + // But, to be fair, neither does Chrome. + validateString(type, 'options.type'); + + if (!isUint32(length)) + throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF); + + super(); + this[kHandle] = createBlob(sources_, length); + this[kLength] = length; + this[kType] = RegExpPrototypeTest(/[^\u{0020}-\u{007E}]/u, type) ? + '' : StringPrototypeToLowerCase(type); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1 + }; + + return `Blob ${inspect({ + size: this.size, + type: this.type, + }, opts)}`; + } + + [kClone]() { + const handle = this[kHandle]; + const type = this[kType]; + const length = this[kLength]; + return { + data: { handle, type, length }, + deserializeInfo: 'internal/blob:InternalBlob' + }; + } + + [kDeserialize]({ handle, type, length }) { + this[kHandle] = handle; + this[kType] = type; + this[kLength] = length; + } + + get type() { return this[kType]; } + + get size() { return this[kLength]; } + + slice(start = 0, end = (this[kLength]), type = this[kType]) { + validateUint32(start, 'start'); + if (end < 0) end = this[kLength] + end; + validateUint32(end, 'end'); + validateString(type, 'type'); + if (end < start) + throw new ERR_OUT_OF_RANGE('end', 'greater than start', end); + if (end > this[kLength]) + throw new ERR_OUT_OF_RANGE('end', 'less than or equal to length', end); + return new InternalBlob( + this[kHandle].slice(start, end), + end - start, type); + } + + async arrayBuffer() { + const job = new FixedSizeBlobCopyJob(this[kHandle]); + + const ret = job.run(); + if (ret !== undefined) + return PromiseResolve(ret); + + const { + promise, + resolve, + reject + } = deferred(); + job.ondone = (err, ab) => { + if (err !== undefined) + return reject(new AbortError()); + resolve(ab); + }; + + return promise; + } + + async text() { + const dec = new TextDecoder(); + return dec.decode(await this.arrayBuffer()); + } +} + +InternalBlob.prototype.constructor = Blob; +ObjectSetPrototypeOf( + InternalBlob.prototype, + Blob.prototype); + +module.exports = { + Blob, + InternalBlob, + isBlob, +}; diff --git a/node.gyp b/node.gyp index 58410ddbd577ba..473af0b3419779 100644 --- a/node.gyp +++ b/node.gyp @@ -111,6 +111,7 @@ 'lib/internal/assert/assertion_error.js', 'lib/internal/assert/calltracker.js', 'lib/internal/async_hooks.js', + 'lib/internal/blob.js', 'lib/internal/blocklist.js', 'lib/internal/buffer.js', 'lib/internal/cli_table.js', @@ -609,6 +610,7 @@ 'src/node.cc', 'src/node_api.cc', 'src/node_binding.cc', + 'src/node_blob.cc', 'src/node_buffer.cc', 'src/node_config.cc', 'src/node_constants.cc', @@ -707,6 +709,7 @@ 'src/node_api.h', 'src/node_api_types.h', 'src/node_binding.h', + 'src/node_blob.h', 'src/node_buffer.h', 'src/node_constants.h', 'src/node_context_data.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 1a82a5bfce846c..90f43f64e521db 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -38,6 +38,7 @@ namespace node { V(ELDHISTOGRAM) \ V(FILEHANDLE) \ V(FILEHANDLECLOSEREQ) \ + V(FIXEDSIZEBLOBCOPY) \ V(FSEVENTWRAP) \ V(FSREQCALLBACK) \ V(FSREQPROMISE) \ diff --git a/src/env.h b/src/env.h index bd5aecd4529ee3..49a1af90058347 100644 --- a/src/env.h +++ b/src/env.h @@ -448,6 +448,7 @@ constexpr size_t kFsStatsBufferLength = V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ V(base_object_ctor_template, v8::FunctionTemplate) \ V(binding_data_ctor_template, v8::FunctionTemplate) \ + V(blob_constructor_template, v8::FunctionTemplate) \ V(blocklist_instance_template, v8::ObjectTemplate) \ V(compiled_fn_entry_template, v8::ObjectTemplate) \ V(dir_instance_template, v8::ObjectTemplate) \ diff --git a/src/node_blob.cc b/src/node_blob.cc new file mode 100644 index 00000000000000..da78662366a842 --- /dev/null +++ b/src/node_blob.cc @@ -0,0 +1,336 @@ +#include "node_blob.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "threadpoolwork-inl.h" +#include "v8.h" + +#include + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Local; +using v8::MaybeLocal; +using v8::Number; +using v8::Object; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +void Blob::Initialize(Environment* env, v8::Local target) { + env->SetMethod(target, "createBlob", New); + FixedSizeBlobCopyJob::Initialize(env, target); +} + +Local Blob::GetConstructorTemplate(Environment* env) { + Local tmpl = env->blob_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->InstanceTemplate()->SetInternalFieldCount(1); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->SetClassName( + FIXED_ONE_BYTE_STRING(env->isolate(), "Blob")); + env->SetProtoMethod(tmpl, "toArrayBuffer", ToArrayBuffer); + env->SetProtoMethod(tmpl, "slice", ToSlice); + env->set_blob_constructor_template(tmpl); + } + return tmpl; +} + +bool Blob::HasInstance(Environment* env, v8::Local object) { + return GetConstructorTemplate(env)->HasInstance(object); +} + +BaseObjectPtr Blob::Create( + Environment* env, + const std::vector store, + size_t length) { + + HandleScope scope(env->isolate()); + + Local ctor; + if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor)) + return BaseObjectPtr(); + + Local obj; + if (!ctor->NewInstance(env->context()).ToLocal(&obj)) + return BaseObjectPtr(); + + return MakeBaseObject(env, obj, store, length); +} + +void Blob::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsArray()); // sources + CHECK(args[1]->IsUint32()); // length + + std::vector entries; + + size_t length = args[1].As()->Value(); + size_t len = 0; + Local ary = args[0].As(); + for (size_t n = 0; n < ary->Length(); n++) { + Local entry; + if (!ary->Get(env->context(), n).ToLocal(&entry)) + return; + CHECK(entry->IsArrayBufferView() || Blob::HasInstance(env, entry)); + if (entry->IsArrayBufferView()) { + Local view = entry.As(); + CHECK_EQ(view->ByteOffset(), 0); + std::shared_ptr store = view->Buffer()->GetBackingStore(); + size_t byte_length = view->ByteLength(); + view->Buffer()->Detach(); // The Blob will own the backing store now. + entries.emplace_back(BlobEntry{std::move(store), byte_length, 0}); + len += byte_length; + } else { + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, entry); + auto source = blob->entries(); + entries.insert(entries.end(), source.begin(), source.end()); + len += blob->length(); + } + } + CHECK_EQ(length, len); + + BaseObjectPtr blob = Create(env, entries, length); + if (blob) + args.GetReturnValue().Set(blob->object()); +} + +void Blob::ToArrayBuffer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args.Holder()); + Local ret; + if (blob->GetArrayBuffer(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Blob::ToSlice(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args.Holder()); + CHECK(args[0]->IsUint32()); + CHECK(args[1]->IsUint32()); + size_t start = args[0].As()->Value(); + size_t end = args[1].As()->Value(); + BaseObjectPtr slice = blob->Slice(env, start, end); + if (slice) + args.GetReturnValue().Set(slice->object()); +} + +void Blob::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("store", length_); +} + +MaybeLocal Blob::GetArrayBuffer(Environment* env) { + EscapableHandleScope scope(env->isolate()); + size_t len = length(); + std::shared_ptr store = + ArrayBuffer::NewBackingStore(env->isolate(), len); + if (len > 0) { + unsigned char* dest = static_cast(store->Data()); + size_t total = 0; + for (const auto& entry : entries()) { + unsigned char* src = static_cast(entry.store->Data()); + src += entry.offset; + memcpy(dest, src, entry.length); + dest += entry.length; + total += entry.length; + CHECK_LE(total, len); + } + } + + return scope.Escape(ArrayBuffer::New(env->isolate(), store)); +} + +BaseObjectPtr Blob::Slice(Environment* env, size_t start, size_t end) { + CHECK_LE(start, length()); + CHECK_LE(end, length()); + CHECK_LE(start, end); + + std::vector slices; + size_t total = end - start; + size_t remaining = total; + + if (total == 0) return Create(env, slices, 0); + + for (const auto& entry : entries()) { + if (start + entry.offset > entry.store->ByteLength()) { + start -= entry.length; + continue; + } + + size_t offset = entry.offset + start; + size_t len = std::min(remaining, entry.store->ByteLength() - offset); + slices.emplace_back(BlobEntry{entry.store, len, offset}); + + remaining -= len; + start = 0; + + if (remaining == 0) + break; + } + + return Create(env, slices, total); +} + +Blob::Blob( + Environment* env, + v8::Local obj, + const std::vector& store, + size_t length) + : BaseObject(env, obj), + store_(store), + length_(length) { + MakeWeak(); +} + +BaseObjectPtr +Blob::BlobTransferData::Deserialize( + Environment* env, + Local context, + std::unique_ptr self) { + if (context != env->context()) { + THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); + return {}; + } + return Blob::Create(env, store_, length_); +} + +BaseObject::TransferMode Blob::GetTransferMode() const { + return BaseObject::TransferMode::kCloneable; +} + +std::unique_ptr Blob::CloneForMessaging() const { + return std::make_unique(store_, length_); +} + +FixedSizeBlobCopyJob::FixedSizeBlobCopyJob( + Environment* env, + Local object, + Blob* blob, + FixedSizeBlobCopyJob::Mode mode) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_FIXEDSIZEBLOBCOPY), + ThreadPoolWork(env), + mode_(mode) { + if (mode == FixedSizeBlobCopyJob::Mode::SYNC) MakeWeak(); + source_ = blob->entries(); + length_ = blob->length(); +} + +void FixedSizeBlobCopyJob::AfterThreadPoolWork(int status) { + Environment* env = AsyncWrap::env(); + CHECK_EQ(mode_, Mode::ASYNC); + CHECK(status == 0 || status == UV_ECANCELED); + std::unique_ptr ptr(this); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local args[2]; + + if (status == UV_ECANCELED) { + args[0] = Number::New(env->isolate(), status), + args[1] = Undefined(env->isolate()); + } else { + args[0] = Undefined(env->isolate()); + args[1] = ArrayBuffer::New(env->isolate(), destination_); + } + + ptr->MakeCallback(env->ondone_string(), arraysize(args), args); +} + +void FixedSizeBlobCopyJob::DoThreadPoolWork() { + Environment* env = AsyncWrap::env(); + destination_ = ArrayBuffer::NewBackingStore(env->isolate(), length_); + unsigned char* dest = static_cast(destination_->Data()); + if (length_ > 0) { + size_t total = 0; + for (const auto& entry : source_) { + unsigned char* src = static_cast(entry.store->Data()); + src += entry.offset; + memcpy(dest, src, entry.length); + dest += entry.length; + total += entry.length; + CHECK_LE(total, length_); + } + } +} + +void FixedSizeBlobCopyJob::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("source", length_); + tracker->TrackFieldWithSize( + "destination", + destination_ ? destination_->ByteLength() : 0); +} + +void FixedSizeBlobCopyJob::Initialize(Environment* env, Local target) { + v8::Local job = env->NewFunctionTemplate(New); + job->Inherit(AsyncWrap::GetConstructorTemplate(env)); + job->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + env->SetProtoMethod(job, "run", Run); + env->SetConstructorFunction(target, "FixedSizeBlobCopyJob", job); +} + +void FixedSizeBlobCopyJob::New(const FunctionCallbackInfo& args) { + static constexpr size_t kMaxSyncLength = 4096; + static constexpr size_t kMaxEntryCount = 4; + + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsObject()); + CHECK(Blob::HasInstance(env, args[0])); + + Blob* blob; + ASSIGN_OR_RETURN_UNWRAP(&blob, args[0]); + + // This is a fairly arbitrary heuristic. We want to avoid deferring to + // the threadpool if the amount of data being copied is small and there + // aren't that many entries to copy. + FixedSizeBlobCopyJob::Mode mode = + (blob->length() < kMaxSyncLength && + blob->entries().size() < kMaxEntryCount) ? + FixedSizeBlobCopyJob::Mode::SYNC : + FixedSizeBlobCopyJob::Mode::ASYNC; + + new FixedSizeBlobCopyJob(env, args.This(), blob, mode); +} + +void FixedSizeBlobCopyJob::Run(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + FixedSizeBlobCopyJob* job; + ASSIGN_OR_RETURN_UNWRAP(&job, args.Holder()); + if (job->mode() == FixedSizeBlobCopyJob::Mode::ASYNC) + return job->ScheduleWork(); + + job->DoThreadPoolWork(); + args.GetReturnValue().Set( + ArrayBuffer::New(env->isolate(), job->destination_)); +} + +void FixedSizeBlobCopyJob::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(Run); +} + +void Blob::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(Blob::New); + registry->Register(Blob::ToArrayBuffer); + registry->Register(Blob::ToSlice); +} + +} // namespace node diff --git a/src/node_blob.h b/src/node_blob.h new file mode 100644 index 00000000000000..965f65390bdd41 --- /dev/null +++ b/src/node_blob.h @@ -0,0 +1,137 @@ +#ifndef SRC_NODE_BLOB_H_ +#define SRC_NODE_BLOB_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "memory_tracker.h" +#include "node_internals.h" +#include "node_worker.h" +#include "v8.h" + +#include + +namespace node { + +struct BlobEntry { + std::shared_ptr store; + size_t length; + size_t offset; +}; + +class Blob : public BaseObject { + public: + static void RegisterExternalReferences( + ExternalReferenceRegistry* registry); + static void Initialize(Environment* env, v8::Local target); + + static void New(const v8::FunctionCallbackInfo& args); + static void ToArrayBuffer(const v8::FunctionCallbackInfo& args); + static void ToSlice(const v8::FunctionCallbackInfo& args); + + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create( + Environment* env, + const std::vector store, + size_t length); + + static bool HasInstance(Environment* env, v8::Local object); + + const std::vector entries() const { + return store_; + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Blob); + SET_SELF_SIZE(Blob); + + // Copies the contents of the Blob into an ArrayBuffer. + v8::MaybeLocal GetArrayBuffer(Environment* env); + + BaseObjectPtr Slice(Environment* env, size_t start, size_t end); + + inline size_t length() const { return length_; } + + class BlobTransferData : public worker::TransferData { + public: + explicit BlobTransferData( + const std::vector& store, + size_t length) + : store_(store), + length_(length) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) override; + + SET_MEMORY_INFO_NAME(BlobTransferData) + SET_SELF_SIZE(BlobTransferData) + SET_NO_MEMORY_INFO() + + private: + std::vector store_; + size_t length_ = 0; + }; + + BaseObject::TransferMode GetTransferMode() const override; + std::unique_ptr CloneForMessaging() const override; + + Blob( + Environment* env, + v8::Local obj, + const std::vector& store, + size_t length); + + private: + std::vector store_; + size_t length_ = 0; +}; + +class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork { + public: + enum class Mode { + SYNC, + ASYNC + }; + + static void RegisterExternalReferences( + ExternalReferenceRegistry* registry); + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + static void Run(const v8::FunctionCallbackInfo& args); + + bool IsNotIndicativeOfMemoryLeakAtExit() const override { + return true; + } + + void DoThreadPoolWork() override; + void AfterThreadPoolWork(int status) override; + + Mode mode() const { return mode_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(FixedSizeBlobCopyJob) + SET_SELF_SIZE(FixedSizeBlobCopyJob) + + private: + FixedSizeBlobCopyJob( + Environment* env, + v8::Local object, + Blob* blob, + Mode mode = Mode::ASYNC); + + Mode mode_; + std::vector source_; + std::shared_ptr destination_; + size_t length_ = 0; +}; + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_NODE_BLOB_H_ diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 77efa7bae2fea5..bc4d239373a448 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -22,6 +22,7 @@ #include "node_buffer.h" #include "allocated_buffer-inl.h" #include "node.h" +#include "node_blob.h" #include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" @@ -1197,6 +1198,8 @@ void Initialize(Local target, env->SetMethod(target, "utf8Write", StringWrite); env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle); + + Blob::Initialize(env, target); } } // anonymous namespace @@ -1235,6 +1238,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(StringWrite); registry->Register(StringWrite); registry->Register(GetZeroFillToggle); + + Blob::RegisterExternalReferences(registry); + FixedSizeBlobCopyJob::RegisterExternalReferences(registry); } } // namespace Buffer diff --git a/test/fixtures/wpt/FileAPI/BlobURL/support/file_test2.txt b/test/fixtures/wpt/FileAPI/BlobURL/support/file_test2.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html b/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html new file mode 100644 index 00000000000000..07fb27ef8af10b --- /dev/null +++ b/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html @@ -0,0 +1,62 @@ + + + + + Blob and File reference URL Test(2) + + + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file.
  6. +
  7. Click the 'start' button.
  8. +
+
+ +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html b/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html new file mode 100644 index 00000000000000..6a03243f934081 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html @@ -0,0 +1,33 @@ + + +File API Test: Progress Event - bubbles, cancelable + + + + +
+ + diff --git a/test/fixtures/wpt/FileAPI/FileReader/support/file_test1.txt b/test/fixtures/wpt/FileAPI/FileReader/support/file_test1.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html new file mode 100644 index 00000000000000..b8c3f84d2bf23a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html @@ -0,0 +1,72 @@ + + + + + FileReader Errors Test + + + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file.
  6. +
  7. Click the 'start' button.
  8. +
+
+ +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html new file mode 100644 index 00000000000000..46d73598a0f91a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html @@ -0,0 +1,42 @@ + + +FileReader NotReadableError Test + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file's readable permission.
  6. +
  7. Click the 'start' button.
  8. +
+
+ + + diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html new file mode 100644 index 00000000000000..add93ed69d139a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html @@ -0,0 +1,40 @@ + + +FileReader SecurityError Test + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Select a system sensitive file (e.g. files in /usr/bin, password files, + and other native operating system executables) in the file inputbox.
  2. +
  3. Click the 'start' button.
  4. +
+
+ + diff --git a/test/fixtures/wpt/FileAPI/FileReader/workers.html b/test/fixtures/wpt/FileAPI/FileReader/workers.html new file mode 100644 index 00000000000000..8e114eeaf86ff5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/workers.html @@ -0,0 +1,27 @@ + + + + + diff --git a/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js b/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js new file mode 100644 index 00000000000000..3d7a0222f31266 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js @@ -0,0 +1,56 @@ +importScripts("/resources/testharness.js"); + +var blob, empty_blob, readerSync; +setup(() => { + readerSync = new FileReaderSync(); + blob = new Blob(["test"]); + empty_blob = new Blob(); +}); + +test(() => { + assert_true(readerSync instanceof FileReaderSync); +}, "Interface"); + +test(() => { + var text = readerSync.readAsText(blob); + assert_equals(text, "test"); +}, "readAsText"); + +test(() => { + var text = readerSync.readAsText(empty_blob); + assert_equals(text, ""); +}, "readAsText with empty blob"); + +test(() => { + var data = readerSync.readAsDataURL(blob); + assert_equals(data.indexOf("data:"), 0); +}, "readAsDataURL"); + +test(() => { + var data = readerSync.readAsDataURL(empty_blob); + assert_equals(data.indexOf("data:"), 0); +}, "readAsDataURL with empty blob"); + +test(() => { + var data = readerSync.readAsBinaryString(blob); + assert_equals(data, "test"); +}, "readAsBinaryString"); + +test(() => { + var data = readerSync.readAsBinaryString(empty_blob); + assert_equals(data, ""); +}, "readAsBinaryString with empty blob"); + +test(() => { + var data = readerSync.readAsArrayBuffer(blob); + assert_true(data instanceof ArrayBuffer); + assert_equals(data.byteLength, "test".length); +}, "readAsArrayBuffer"); + +test(() => { + var data = readerSync.readAsArrayBuffer(empty_blob); + assert_true(data instanceof ArrayBuffer); + assert_equals(data.byteLength, 0); +}, "readAsArrayBuffer with empty blob"); + +done(); diff --git a/test/fixtures/wpt/FileAPI/META.yml b/test/fixtures/wpt/FileAPI/META.yml new file mode 100644 index 00000000000000..506a59fec1eb33 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/META.yml @@ -0,0 +1,6 @@ +spec: https://w3c.github.io/FileAPI/ +suggested_reviewers: + - inexorabletash + - zqzhang + - jdm + - mkruisselbrink diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer.any.js new file mode 100644 index 00000000000000..2310646e5fdeab --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer.any.js @@ -0,0 +1,45 @@ +// META: title=Blob Array Buffer +// META: script=../support/Blob.js +'use strict'; + +promise_test(async () => { + const input_arr = new TextEncoder().encode("PASS"); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer()") + +promise_test(async () => { + const input_arr = new TextEncoder().encode(""); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer() empty Blob data") + +promise_test(async () => { + const input_arr = new TextEncoder().encode("\u08B8\u000a"); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer() non-ascii input") + +promise_test(async () => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + const blob = new Blob([typed_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_equals_typed_array(new Uint8Array(array_buffer), typed_arr); +}, "Blob.arrayBuffer() non-unicode input") + +promise_test(async () => { + const input_arr = new TextEncoder().encode("PASS"); + const blob = new Blob([input_arr]); + const array_buffer_results = await Promise.all([blob.arrayBuffer(), + blob.arrayBuffer(), blob.arrayBuffer()]); + for (let array_buffer of array_buffer_results) { + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); + } +}, "Blob.arrayBuffer() concurrent reads") diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.html b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.html new file mode 100644 index 00000000000000..04edd2a303b135 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.html @@ -0,0 +1,104 @@ + + +Blob constructor: endings option + + + + diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.html b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.html new file mode 100644 index 00000000000000..62a649aed66418 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.html @@ -0,0 +1,501 @@ + + +Blob constructor + + + + + + + +
+ diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js b/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js new file mode 100644 index 00000000000000..a67060e7b85eff --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js @@ -0,0 +1,14 @@ +importScripts("/resources/testharness.js"); + +async_test(function() { + var data = "TEST"; + var blob = new Blob([data], {type: "text/plain"}); + var reader = new FileReader(); + reader.onload = this.step_func_done(function() { + assert_equals(reader.result, data); + }); + reader.onerror = this.unreached_func("Unexpected error event"); + reader.readAsText(blob); +}, "Create Blob in Worker"); + +done(); diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.html b/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.html new file mode 100644 index 00000000000000..74cd83a34f7116 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.html @@ -0,0 +1,42 @@ + + +Blob slice overflow + + + + +
+ + diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-slice.html b/test/fixtures/wpt/FileAPI/blob/Blob-slice.html new file mode 100644 index 00000000000000..03fe6ca5343bd1 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-slice.html @@ -0,0 +1,238 @@ + + +Blob slice + + + + + +
+ diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js new file mode 100644 index 00000000000000..792b6639c35a26 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js @@ -0,0 +1,72 @@ +// META: title=Blob Stream +// META: script=../support/Blob.js +// META: script=../../streams/resources/test-utils.js +'use strict'; + +// Helper function that triggers garbage collection while reading a chunk +// if perform_gc is true. +async function read_and_gc(reader, perform_gc) { + const read_promise = reader.read(); + if (perform_gc) + garbageCollect(); + return read_promise; +} + +// Takes in a ReadableStream and reads from it until it is done, returning +// an array that contains the results of each read operation. If perform_gc +// is true, garbage collection is triggered while reading every chunk. +async function read_all_chunks(stream, perform_gc = false) { + assert_true(stream instanceof ReadableStream); + assert_true('getReader' in stream); + const reader = stream.getReader(); + + assert_true('read' in reader); + let read_value = await read_and_gc(reader, perform_gc); + + let out = []; + let i = 0; + while (!read_value.done) { + for (let val of read_value.value) { + out[i++] = val; + } + read_value = await read_and_gc(reader, perform_gc); + } + return out; +} + +promise_test(async () => { + const blob = new Blob(["PASS"]); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + for (let [index, value] of chunks.entries()) { + assert_equals(value, "PASS".charCodeAt(index)); + } +}, "Blob.stream()") + +promise_test(async () => { + const blob = new Blob(); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + assert_array_equals(chunks, []); +}, "Blob.stream() empty Blob") + +promise_test(async () => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + const blob = new Blob([typed_arr]); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + assert_array_equals(chunks, input_arr); +}, "Blob.stream() non-unicode input") + +promise_test(async() => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + let blob = new Blob([typed_arr]); + const stream = blob.stream(); + blob = null; + garbageCollect(); + const chunks = await read_all_chunks(stream, /*perform_gc=*/true); + assert_array_equals(chunks, input_arr); +}, "Blob.stream() garbage collection of blob shouldn't break stream" + + "consumption") diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-text.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-text.any.js new file mode 100644 index 00000000000000..d04fa97cffe6a3 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-text.any.js @@ -0,0 +1,64 @@ +// META: title=Blob Text +// META: script=../support/Blob.js +'use strict'; + +promise_test(async () => { + const blob = new Blob(["PASS"]); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text()") + +promise_test(async () => { + const blob = new Blob(); + const text = await blob.text(); + assert_equals(text, ""); +}, "Blob.text() empty blob data") + +promise_test(async () => { + const blob = new Blob(["P", "A", "SS"]); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text() multi-element array in constructor") + +promise_test(async () => { + const non_unicode = "\u0061\u030A"; + const input_arr = new TextEncoder().encode(non_unicode); + const blob = new Blob([input_arr]); + const text = await blob.text(); + assert_equals(text, non_unicode); +}, "Blob.text() non-unicode") + +promise_test(async () => { + const blob = new Blob(["PASS"], { type: "text/plain;charset=utf-16le" }); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text() different charset param in type option") + +promise_test(async () => { + const non_unicode = "\u0061\u030A"; + const input_arr = new TextEncoder().encode(non_unicode); + const blob = new Blob([input_arr], { type: "text/plain;charset=utf-16le" }); + const text = await blob.text(); + assert_equals(text, non_unicode); +}, "Blob.text() different charset param with non-ascii input") + +promise_test(async () => { + const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, + 252, 253, 254, 255]); + const blob = new Blob([input_arr]); + const text = await blob.text(); + assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\ufffd\ufffd\ufffd\ufffd"); +}, "Blob.text() invalid utf-8 input") + +promise_test(async () => { + const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, + 252, 253, 254, 255]); + const blob = new Blob([input_arr]); + const text_results = await Promise.all([blob.text(), blob.text(), + blob.text()]); + for (let text of text_results) { + assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\ufffd\ufffd\ufffd\ufffd"); + } +}, "Blob.text() concurrent reads") diff --git a/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html new file mode 100644 index 00000000000000..1282b6c5ac2c79 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html @@ -0,0 +1,104 @@ + + +File constructor: endings option + + + + diff --git a/test/fixtures/wpt/FileAPI/file/File-constructor.html b/test/fixtures/wpt/FileAPI/file/File-constructor.html new file mode 100644 index 00000000000000..3477e4ada16e92 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/File-constructor.html @@ -0,0 +1,159 @@ + + +File constructor + + + +
+ diff --git a/test/fixtures/wpt/FileAPI/file/Worker-read-file-constructor.worker.js b/test/fixtures/wpt/FileAPI/file/Worker-read-file-constructor.worker.js new file mode 100644 index 00000000000000..4e003b3c958a94 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/Worker-read-file-constructor.worker.js @@ -0,0 +1,15 @@ +importScripts("/resources/testharness.js"); + +async_test(function() { + var file = new File(["bits"], "dummy", { 'type': 'text/plain', lastModified: 42 }); + var reader = new FileReader(); + reader.onload = this.step_func_done(function() { + assert_equals(file.name, "dummy", "file name"); + assert_equals(reader.result, "bits", "file content"); + assert_equals(file.lastModified, 42, "file lastModified"); + }); + reader.onerror = this.unreached_func("Unexpected error event"); + reader.readAsText(file); +}, "FileReader in Worker"); + +done(); diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html new file mode 100644 index 00000000000000..d11f4a860931b4 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html @@ -0,0 +1,117 @@ + + +Upload files named using controls (tentative) + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html new file mode 100644 index 00000000000000..659af3bde85852 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html @@ -0,0 +1,72 @@ + + + +Upload files in ISO-2022-JP form (tentative) + + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html new file mode 100644 index 00000000000000..5c2d6d0bf1fe01 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html @@ -0,0 +1,230 @@ + + +Upload files named using punctuation (tentative) + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-utf-8.html b/test/fixtures/wpt/FileAPI/file/send-file-form-utf-8.html new file mode 100644 index 00000000000000..1be44f4f4db09e --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-utf-8.html @@ -0,0 +1,62 @@ + + +Upload files in UTF-8 form + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html new file mode 100644 index 00000000000000..a2c37186b3e023 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html @@ -0,0 +1,69 @@ + + +Upload files in Windows-1252 form (tentative) + + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html new file mode 100644 index 00000000000000..503b08a51706f7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html @@ -0,0 +1,70 @@ + + +Upload files in x-user-defined form (tentative) + + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form.html b/test/fixtures/wpt/FileAPI/file/send-file-form.html new file mode 100644 index 00000000000000..baa8d4286c5789 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form.html @@ -0,0 +1,25 @@ + + +Upload ASCII-named file in UTF-8 form + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html new file mode 100644 index 00000000000000..4259741b63ef31 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html @@ -0,0 +1,93 @@ + + +FormData: Upload files named using controls (tentative) + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html new file mode 100644 index 00000000000000..d8e84e9d978094 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html @@ -0,0 +1,168 @@ + + +FormData: Upload files named using punctuation (tentative) + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html new file mode 100644 index 00000000000000..7a7f6cefe776b9 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html @@ -0,0 +1,53 @@ + + +FormData: Upload files in UTF-8 fetch() + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata.html new file mode 100644 index 00000000000000..77e048e54741c0 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-formdata.html @@ -0,0 +1,28 @@ + + +FormData: Upload ASCII-named file in UTF-8 form + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/fileReader.html b/test/fixtures/wpt/FileAPI/fileReader.html new file mode 100644 index 00000000000000..b767e22d4a66eb --- /dev/null +++ b/test/fixtures/wpt/FileAPI/fileReader.html @@ -0,0 +1,67 @@ + + + + FileReader States + + + + + + +
+ + + diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist.html new file mode 100644 index 00000000000000..b97dcde19f647c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/filelist-section/filelist.html @@ -0,0 +1,57 @@ + + + + + FileAPI Test: filelist + + + + + + + + + +
+ +
+
+ + + + + diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html new file mode 100644 index 00000000000000..2efaa059fa4897 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html @@ -0,0 +1,64 @@ + + + + + FileAPI Test: filelist_multiple_selected_files + + + + + + + + + +
+ +
+
+

Test steps:

+
    +
  1. Download upload.txt, upload.zip to local.
  2. +
  3. Select the local two files (upload.txt, upload.zip) to run the test.
  4. +
+
+ +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html new file mode 100644 index 00000000000000..966aadda615589 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html @@ -0,0 +1,64 @@ + + + + + FileAPI Test: filelist_selected_file + + + + + + + + + +
+ +
+
+

Test steps:

+
    +
  1. Download upload.txt to local.
  2. +
  3. Select the local upload.txt file to run the test.
  4. +
+
+ +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt new file mode 100644 index 00000000000000..f45965b711f127 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt @@ -0,0 +1 @@ +Hello, this is test file for file upload. diff --git a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip new file mode 100644 index 00000000000000..41bfebe5eed561 Binary files /dev/null and b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip differ diff --git a/test/fixtures/wpt/FileAPI/historical.https.html b/test/fixtures/wpt/FileAPI/historical.https.html new file mode 100644 index 00000000000000..4f841f17639459 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/historical.https.html @@ -0,0 +1,65 @@ + + + + + Historical features + + + + + +
+ + + diff --git a/test/fixtures/wpt/FileAPI/idlharness-manual.html b/test/fixtures/wpt/FileAPI/idlharness-manual.html new file mode 100644 index 00000000000000..c1d8b0c7149d75 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/idlharness-manual.html @@ -0,0 +1,45 @@ + + + + + File API manual IDL tests + + + + + + + + +

File API manual IDL tests

+ +

Either download upload.txt and select it below or select an + arbitrary local file.

+ +
+ +
+ +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/idlharness.html b/test/fixtures/wpt/FileAPI/idlharness.html new file mode 100644 index 00000000000000..5e0a43f80df3f8 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/idlharness.html @@ -0,0 +1,40 @@ + + + + + File API automated IDL tests + + + + + + + + +

File API automated IDL tests

+ +
+ +
+ +
+ + + + + diff --git a/test/fixtures/wpt/FileAPI/idlharness.worker.js b/test/fixtures/wpt/FileAPI/idlharness.worker.js new file mode 100644 index 00000000000000..786b7e4199fb45 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/idlharness.worker.js @@ -0,0 +1,20 @@ +importScripts("/resources/testharness.js"); +importScripts("/resources/WebIDLParser.js", "/resources/idlharness.js"); + +'use strict'; + +// https://w3c.github.io/FileAPI/ + +idl_test( + ['FileAPI'], + ['dom', 'html', 'url'], + idl_array => { + idl_array.add_objects({ + Blob: ['new Blob(["TEST"])'], + File: ['new File(["myFileBits"], "myFileName")'], + FileReader: ['new FileReader()'], + FileReaderSync: ['new FileReaderSync()'] + }); + } +); +done(); diff --git a/test/fixtures/wpt/FileAPI/progress-manual.html b/test/fixtures/wpt/FileAPI/progress-manual.html new file mode 100644 index 00000000000000..b2e03b3eb27387 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/progress-manual.html @@ -0,0 +1,49 @@ + + +Process Events for FileReader + + + + +Please choose one file through this input below.
+ +
+ diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html b/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html new file mode 100644 index 00000000000000..d65ae9db18a1ff --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html @@ -0,0 +1,91 @@ + + +FileAPI Test: Blob Determining Encoding + + + + + +
+ diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html new file mode 100644 index 00000000000000..86657b5711aff1 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html @@ -0,0 +1,23 @@ + + +FileReader event handler attributes + + +
+ diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html new file mode 100644 index 00000000000000..e7279fe4bd445e --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html @@ -0,0 +1,89 @@ + +FileReader: starting new reads while one is in progress + + + + +
+ diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html new file mode 100644 index 00000000000000..940a775d35bf42 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html @@ -0,0 +1,53 @@ + + + + + FileAPI Test: filereader_abort + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html new file mode 100644 index 00000000000000..cf4524825b80ca --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html @@ -0,0 +1,35 @@ + + + + + FileAPI Test: filereader_error + + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js new file mode 100644 index 00000000000000..ac692907d119f7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js @@ -0,0 +1,19 @@ +promise_test(async t => { + var reader = new FileReader(); + var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); + reader.readAsText(new Blob([])); + await eventWatcher.wait_for('loadstart'); + // No progress event for an empty blob, as no data is loaded. + await eventWatcher.wait_for('load'); + await eventWatcher.wait_for('loadend'); +}, 'events are dispatched in the correct order for an empty blob'); + +promise_test(async t => { + var reader = new FileReader(); + var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); + reader.readAsText(new Blob(['a'])); + await eventWatcher.wait_for('loadstart'); + await eventWatcher.wait_for('progress'); + await eventWatcher.wait_for('load'); + await eventWatcher.wait_for('loadend'); +}, 'events are dispatched in the correct order for a non-empty blob'); diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html new file mode 100644 index 00000000000000..702ca9afd7b067 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html @@ -0,0 +1,69 @@ + + + + + FileAPI Test: filereader_file + + + + + + + +
+

Test step:

+
    +
  1. Download blue-100x100.png to local.
  2. +
  3. Select the local file (blue-100x100.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html new file mode 100644 index 00000000000000..fca42c7fceba48 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html @@ -0,0 +1,47 @@ + + + + + FileAPI Test: filereader_file_img + + + + + + + +
+

Test step:

+
    +
  1. Download blue-100x100.png to local.
  2. +
  3. Select the local file (blue-100x100.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html new file mode 100644 index 00000000000000..31001a51a0727f --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html @@ -0,0 +1,38 @@ + + + + + FileAPI Test: filereader_readAsArrayBuffer + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html new file mode 100644 index 00000000000000..b550e4d0a96dc7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html @@ -0,0 +1,32 @@ + + +FileAPI Test: filereader_readAsBinaryString + + + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html new file mode 100644 index 00000000000000..5bc39499a229d1 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html @@ -0,0 +1,51 @@ + + +FileAPI Test: FileReader.readAsDataURL + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html new file mode 100644 index 00000000000000..7d639d0111473b --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html @@ -0,0 +1,51 @@ + + + + + FileAPI Test: filereader_readAsText + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html new file mode 100644 index 00000000000000..1586b8995059f7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html @@ -0,0 +1,34 @@ + + + + + FileAPI Test: filereader_readystate + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html new file mode 100644 index 00000000000000..b80322ed424f83 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html @@ -0,0 +1,97 @@ + + + + + FileAPI Test: filereader_result + + + + + + +
+ + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png b/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png new file mode 100644 index 00000000000000..b662fe18ec4797 Binary files /dev/null and b/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png differ diff --git a/test/fixtures/wpt/FileAPI/support/Blob.js b/test/fixtures/wpt/FileAPI/support/Blob.js new file mode 100644 index 00000000000000..04069acd3ccbe7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/Blob.js @@ -0,0 +1,70 @@ +'use strict' + +function test_blob(fn, expectations) { + var expected = expectations.expected, + type = expectations.type, + desc = expectations.desc; + + var t = async_test(desc); + t.step(function() { + var blob = fn(); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + assert_equals(blob.type, type); + assert_equals(blob.size, expected.length); + + var fr = new FileReader(); + fr.onload = t.step_func_done(function(event) { + assert_equals(this.result, expected); + }, fr); + fr.onerror = t.step_func(function(e) { + assert_unreached("got error event on FileReader"); + }); + fr.readAsText(blob, "UTF-8"); + }); +} + +function test_blob_binary(fn, expectations) { + var expected = expectations.expected, + type = expectations.type, + desc = expectations.desc; + + var t = async_test(desc); + t.step(function() { + var blob = fn(); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + assert_equals(blob.type, type); + assert_equals(blob.size, expected.length); + + var fr = new FileReader(); + fr.onload = t.step_func_done(function(event) { + assert_true(this.result instanceof ArrayBuffer, + "Result should be an ArrayBuffer"); + assert_array_equals(new Uint8Array(this.result), expected); + }, fr); + fr.onerror = t.step_func(function(e) { + assert_unreached("got error event on FileReader"); + }); + fr.readAsArrayBuffer(blob); + }); +} + +// Assert that two TypedArray objects have the same byte values +self.assert_equals_typed_array = (array1, array2) => { + const [view1, view2] = [array1, array2].map((array) => { + assert_true(array.buffer instanceof ArrayBuffer, + 'Expect input ArrayBuffers to contain field `buffer`'); + return new DataView(array.buffer, array.byteOffset, array.byteLength); + }); + + assert_equals(view1.byteLength, view2.byteLength, + 'Expect both arrays to be of the same byte length'); + + const byteLength = view1.byteLength; + + for (let i = 0; i < byteLength; ++i) { + assert_equals(view1.getUint8(i), view2.getUint8(i), + `Expect byte at buffer position ${i} to be equal`); + } +} diff --git a/test/fixtures/wpt/FileAPI/support/document-domain-setter.sub.html b/test/fixtures/wpt/FileAPI/support/document-domain-setter.sub.html new file mode 100644 index 00000000000000..61aebdf326679c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/document-domain-setter.sub.html @@ -0,0 +1,7 @@ + +Relevant/current/blob source page used as a test helper + + diff --git a/test/fixtures/wpt/FileAPI/support/historical-serviceworker.js b/test/fixtures/wpt/FileAPI/support/historical-serviceworker.js new file mode 100644 index 00000000000000..8bd89a23adb70f --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/historical-serviceworker.js @@ -0,0 +1,5 @@ +importScripts('/resources/testharness.js'); + +test(() => { + assert_false('FileReaderSync' in self); +}, '"FileReaderSync" should not be supported in service workers'); diff --git a/test/fixtures/wpt/FileAPI/support/incumbent.sub.html b/test/fixtures/wpt/FileAPI/support/incumbent.sub.html new file mode 100644 index 00000000000000..63a81cd3281c46 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/incumbent.sub.html @@ -0,0 +1,22 @@ + +Incumbent page used as a test helper + + + + + + diff --git a/test/fixtures/wpt/FileAPI/support/send-file-form-helper.js b/test/fixtures/wpt/FileAPI/support/send-file-form-helper.js new file mode 100644 index 00000000000000..d6adf21ec33795 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/send-file-form-helper.js @@ -0,0 +1,282 @@ +'use strict'; + +// See /FileAPI/file/resources/echo-content-escaped.py +function escapeString(string) { + return string.replace(/\\/g, "\\\\").replace( + /[^\x20-\x7E]/g, + (x) => { + let hex = x.charCodeAt(0).toString(16); + if (hex.length < 2) hex = "0" + hex; + return `\\x${hex}`; + }, + ).replace(/\\x0d\\x0a/g, "\r\n"); +} + +// Rationale for this particular test character sequence, which is +// used in filenames and also in file contents: +// +// - ABC~ ensures the string starts with something we can read to +// ensure it is from the correct source; ~ is used because even +// some 1-byte otherwise-ASCII-like parts of ISO-2022-JP +// interpret it differently. +// - ‾¥ are inside a single-byte range of ISO-2022-JP and help +// diagnose problems due to filesystem encoding or locale +// - ≈ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - ¤ is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale; it is also the "simplest" case +// needing substitution in ISO-2022-JP +// - ・ is inside a single-byte range of ISO-2022-JP in some variants +// and helps diagnose problems due to filesystem encoding or locale; +// on the web it is distinct when decoding but unified when encoding +// - ・ is inside a double-byte range of ISO-2022-JP and helps +// diagnose problems due to filesystem encoding or locale +// - • is inside Windows-1252 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes +// - ∙ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - · is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale and also ensures HTML named +// character references (e.g. ·) are not used +// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes +// - ★ is inside ISO-2022-JP on a non-Kanji page and makes correct +// output easier to spot +// - 星 is inside ISO-2022-JP on a Kanji page and makes correct +// output easier to spot +// - 🌟 is outside the BMP and makes incorrect surrogate pair +// substitution detectable and ensures substitutions work +// correctly immediately after Kanji 2-byte ISO-2022-JP +// - 星 repeated here ensures the correct codec state is used +// after a non-BMP substitution +// - ★ repeated here also makes correct output easier to spot +// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes and also ensures +// substitutions work correctly immediately after non-Kanji +// 2-byte ISO-2022-JP +// - · is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale and also ensures HTML named +// character references (e.g. ·) are not used +// - ∙ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - • is inside Windows-1252 and again helps diagnose problems +// due to filesystem encoding or locale +// - ・ is inside a double-byte range of ISO-2022-JP and helps +// diagnose problems due to filesystem encoding or locale +// - ・ is inside a single-byte range of ISO-2022-JP in some variants +// and helps diagnose problems due to filesystem encoding or locale; +// on the web it is distinct when decoding but unified when encoding +// - ¤ is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale; again it is a "simple" +// substitution case +// - ≈ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - ¥‾ are inside a single-byte range of ISO-2022-JP and help +// diagnose problems due to filesystem encoding or locale +// - ~XYZ ensures earlier errors don't lead to misencoding of +// simple ASCII +// +// Overall the near-symmetry makes common I18N mistakes like +// off-by-1-after-non-BMP easier to spot. All the characters +// are also allowed in Windows Unicode filenames. +const kTestChars = 'ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ'; + +// The kTestFallback* strings represent the expected byte sequence from +// encoding kTestChars with the given encoding with "html" replacement +// mode, isomorphic-decoded. That means, characters that can't be +// encoded in that encoding get HTML-escaped, but no further +// `escapeString`-like escapes are needed. +const kTestFallbackUtf8 = ( + "ABC~\xE2\x80\xBE\xC2\xA5\xE2\x89\x88\xC2\xA4\xEF\xBD\xA5\xE3\x83\xBB\xE2" + + "\x80\xA2\xE2\x88\x99\xC2\xB7\xE2\x98\xBC\xE2\x98\x85\xE6\x98\x9F\xF0\x9F" + + "\x8C\x9F\xE6\x98\x9F\xE2\x98\x85\xE2\x98\xBC\xC2\xB7\xE2\x88\x99\xE2\x80" + + "\xA2\xE3\x83\xBB\xEF\xBD\xA5\xC2\xA4\xE2\x89\x88\xC2\xA5\xE2\x80\xBE~XYZ" +); + +const kTestFallbackIso2022jp = ( + ("ABC~\x1B(J~\\≈¤\x1B$B!&!&\x1B(B•∙·☼\x1B$B!z@1\x1B(B🌟" + + "\x1B$B@1!z\x1B(B☼·∙•\x1B$B!&!&\x1B(B¤≈\x1B(J\\~\x1B(B~XYZ") + .replace(/[^\0-\x7F]/gu, (x) => `&#${x.codePointAt(0)};`) +); + +const kTestFallbackWindows1252 = ( + "ABC~‾\xA5≈\xA4・・\x95∙\xB7☼★星🌟星★☼\xB7∙\x95・・\xA4≈\xA5‾~XYZ".replace( + /[^\0-\xFF]/gu, + (x) => `&#${x.codePointAt(0)};`, + ) +); + +const kTestFallbackXUserDefined = kTestChars.replace( + /[^\0-\x7F]/gu, + (x) => `&#${x.codePointAt(0)};`, +); + +// formPostFileUploadTest - verifies multipart upload structure and +// numeric character reference replacement for filenames, field names, +// and field values using form submission. +// +// Uses /FileAPI/file/resources/echo-content-escaped.py to echo the +// upload POST with controls and non-ASCII bytes escaped. This is done +// because navigations whose response body contains [\0\b\v] may get +// treated as a download, which is not what we want. Use the +// `escapeString` function to replicate that kind of escape (note that +// it takes an isomorphic-decoded string, not a byte sequence). +// +// Fields in the parameter object: +// +// - fileNameSource: purely explanatory and gives a clue about which +// character encoding is the source for the non-7-bit-ASCII parts of +// the fileBaseName, or Unicode if no smaller-than-Unicode source +// contains all the characters. Used in the test name. +// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename +// used for the constructed test file. Used in the test name. +// - formEncoding: the acceptCharset of the form used to submit the +// test file. Used in the test name. +// - expectedEncodedBaseName: the expected formEncoding-encoded +// version of fileBaseName, isomorphic-decoded. That means, characters +// that can't be encoded in that encoding get HTML-escaped, but no +// further `escapeString`-like escapes are needed. +const formPostFileUploadTest = ({ + fileNameSource, + fileBaseName, + formEncoding, + expectedEncodedBaseName, +}) => { + promise_test(async testCase => { + + if (document.readyState !== 'complete') { + await new Promise(resolve => addEventListener('load', resolve)); + } + + const formTargetFrame = Object.assign(document.createElement('iframe'), { + name: 'formtargetframe', + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement('form'), { + acceptCharset: formEncoding, + action: '/FileAPI/file/resources/echo-content-escaped.py', + method: 'POST', + enctype: 'multipart/form-data', + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + // Used to verify that the browser agrees with the test about + // which form charset is used. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: '_charset_', + })); + + // Used to verify that the browser agrees with the test about + // field value replacement and encoding independently of file system + // idiosyncracies. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: 'filename', + value: fileBaseName, + })); + + // Same, but with name and value reversed to ensure field names + // get the same treatment. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: fileBaseName, + value: 'filename', + })); + + const fileInput = Object.assign(document.createElement('input'), { + type: 'file', + name: 'file', + }); + form.append(fileInput); + + // Removes c:\fakepath\ or other pseudofolder and returns just the + // final component of filePath; allows both / and \ as segment + // delimiters. + const baseNameOfFilePath = filePath => filePath.split(/[\/\\]/).pop(); + await new Promise(resolve => { + const dataTransfer = new DataTransfer; + dataTransfer.items.add( + new File([kTestChars], fileBaseName, {type: 'text/plain'})); + fileInput.files = dataTransfer.files; + // For historical reasons .value will be prefixed with + // c:\fakepath\, but the basename should match the file name + // exposed through the newer .files[0].name API. This check + // verifies that assumption. + assert_equals( + baseNameOfFilePath(fileInput.files[0].name), + baseNameOfFilePath(fileInput.value), + `The basename of the field's value should match its files[0].name`); + form.submit(); + formTargetFrame.onload = resolve; + }); + + const formDataText = formTargetFrame.contentDocument.body.textContent; + const formDataLines = formDataText.split('\n'); + if (formDataLines.length && !formDataLines[formDataLines.length - 1]) { + --formDataLines.length; + } + assert_greater_than( + formDataLines.length, + 2, + `${fileBaseName}: multipart form data must have at least 3 lines: ${ + JSON.stringify(formDataText) + }`); + const boundary = formDataLines[0]; + assert_equals( + formDataLines[formDataLines.length - 1], + boundary + '--', + `${fileBaseName}: multipart form data must end with ${boundary}--: ${ + JSON.stringify(formDataText) + }`); + + const asValue = expectedEncodedBaseName.replace(/\r\n?|\n/g, "\r\n"); + const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent); + const asFilename = expectedEncodedBaseName.replace(/[\r\n"]/g, encodeURIComponent); + + // The response body from echo-content-escaped.py has controls and non-ASCII + // bytes escaped, so any caller-provided field that might contain such bytes + // must be passed to `escapeString`, after any other expected + // transformations. + const expectedText = [ + boundary, + 'Content-Disposition: form-data; name="_charset_"', + '', + formEncoding, + boundary, + 'Content-Disposition: form-data; name="filename"', + '', + // Unlike for names and filenames, multipart/form-data values don't escape + // \r\n linebreaks, and when they're read from an iframe they become \n. + escapeString(asValue).replace(/\r\n/g, "\n"), + boundary, + `Content-Disposition: form-data; name="${escapeString(asName)}"`, + '', + 'filename', + boundary, + `Content-Disposition: form-data; name="file"; ` + + `filename="${escapeString(asFilename)}"`, + 'Content-Type: text/plain', + '', + escapeString(kTestFallbackUtf8), + boundary + '--', + ].join('\n'); + + assert_true( + formDataText.startsWith(expectedText), + `Unexpected multipart-shaped form data received:\n${ + formDataText + }\nExpected:\n${expectedText}`); + }, `Upload ${fileBaseName} (${fileNameSource}) in ${formEncoding} form`); +}; diff --git a/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js b/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js new file mode 100644 index 00000000000000..53572ef36c8d1b --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js @@ -0,0 +1,97 @@ +"use strict"; + +const kTestChars = "ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ"; + +// formDataPostFileUploadTest - verifies multipart upload structure and +// numeric character reference replacement for filenames, field names, +// and field values using FormData and fetch(). +// +// Uses /fetch/api/resources/echo-content.py to echo the upload +// POST (unlike in send-file-form-helper.js, here we expect all +// multipart/form-data request bodies to be UTF-8, so we don't need to +// escape controls and non-ASCII bytes). +// +// Fields in the parameter object: +// +// - fileNameSource: purely explanatory and gives a clue about which +// character encoding is the source for the non-7-bit-ASCII parts of +// the fileBaseName, or Unicode if no smaller-than-Unicode source +// contains all the characters. Used in the test name. +// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename +// used for the constructed test file. Used in the test name. +const formDataPostFileUploadTest = ({ + fileNameSource, + fileBaseName, +}) => { + promise_test(async (testCase) => { + const formData = new FormData(); + let file = new Blob([kTestChars], { type: "text/plain" }); + try { + // Switch to File in browsers that allow this + file = new File([file], fileBaseName, { type: file.type }); + } catch (ignoredException) { + } + + // Used to verify that the browser agrees with the test about + // field value replacement and encoding independently of file system + // idiosyncracies. + formData.append("filename", fileBaseName); + + // Same, but with name and value reversed to ensure field names + // get the same treatment. + formData.append(fileBaseName, "filename"); + + formData.append("file", file, fileBaseName); + + const formDataText = await (await fetch( + `/fetch/api/resources/echo-content.py`, + { + method: "POST", + body: formData, + }, + )).text(); + const formDataLines = formDataText.split("\r\n"); + if (formDataLines.length && !formDataLines[formDataLines.length - 1]) { + --formDataLines.length; + } + assert_greater_than( + formDataLines.length, + 2, + `${fileBaseName}: multipart form data must have at least 3 lines: ${ + JSON.stringify(formDataText) + }`, + ); + const boundary = formDataLines[0]; + assert_equals( + formDataLines[formDataLines.length - 1], + boundary + "--", + `${fileBaseName}: multipart form data must end with ${boundary}--: ${ + JSON.stringify(formDataText) + }`, + ); + + const asName = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent); + const expectedText = [ + boundary, + 'Content-Disposition: form-data; name="filename"', + "", + fileBaseName, + boundary, + `Content-Disposition: form-data; name="${asName}"`, + "", + "filename", + boundary, + `Content-Disposition: form-data; name="file"; ` + + `filename="${asName}"`, + "Content-Type: text/plain", + "", + kTestChars, + boundary + "--", + ].join("\r\n"); + + assert_true( + formDataText.startsWith(expectedText), + `Unexpected multipart-shaped form data received:\n${formDataText}\nExpected:\n${expectedText}`, + ); + }, `Upload ${fileBaseName} (${fileNameSource}) in fetch with FormData`); +}; diff --git a/test/fixtures/wpt/FileAPI/support/upload.txt b/test/fixtures/wpt/FileAPI/support/upload.txt new file mode 100644 index 00000000000000..5ab2f8a4323aba --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/upload.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/support/url-origin.html b/test/fixtures/wpt/FileAPI/support/url-origin.html new file mode 100644 index 00000000000000..63755113915f9f --- /dev/null +++ b/test/fixtures/wpt/FileAPI/support/url-origin.html @@ -0,0 +1,6 @@ + + diff --git a/test/fixtures/wpt/FileAPI/unicode.html b/test/fixtures/wpt/FileAPI/unicode.html new file mode 100644 index 00000000000000..ce3e3579d7c2c7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/unicode.html @@ -0,0 +1,46 @@ + + +Blob/Unicode interaction: normalization and encoding + + + diff --git a/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html b/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html new file mode 100644 index 00000000000000..21b8c5bb1986d5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html @@ -0,0 +1,61 @@ + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html b/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html new file mode 100644 index 00000000000000..0052b26fa62130 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html @@ -0,0 +1,26 @@ + + +Blob URL serialization (specifically the origin) in multi-global situations + + + + + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/url/resources/create-helper.html b/test/fixtures/wpt/FileAPI/url/resources/create-helper.html new file mode 100644 index 00000000000000..fa6cf4e671e835 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/resources/create-helper.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/create-helper.js b/test/fixtures/wpt/FileAPI/url/resources/create-helper.js new file mode 100644 index 00000000000000..e6344f700ced60 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/resources/create-helper.js @@ -0,0 +1,4 @@ +self.addEventListener('message', e => { + let url = URL.createObjectURL(e.data.blob); + self.postMessage({url: url}); +}); diff --git a/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js b/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js new file mode 100644 index 00000000000000..a81ea1e7b1de35 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js @@ -0,0 +1,71 @@ +// This method generates a number of tests verifying fetching of blob URLs, +// allowing the same tests to be used both with fetch() and XMLHttpRequest. +// +// |fetch_method| is only used in test names, and should describe the +// (javascript) method being used by the other two arguments (i.e. 'fetch' or 'XHR'). +// +// |fetch_should_succeed| is a callback that is called with the Test and a URL. +// Fetching the URL is expected to succeed. The callback should return a promise +// resolved with whatever contents were fetched. +// +// |fetch_should_fail| similarly is a callback that is called with the Test, a URL +// to fetch, and optionally a method to use to do the fetch. If no method is +// specified the callback should use the 'GET' method. Fetching of these URLs is +// expected to fail, and the callback should return a promise that resolves iff +// fetching did indeed fail. +function fetch_tests(fetch_method, fetch_should_succeed, fetch_should_fail) { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + }, 'Blob URLs can be used in ' + fetch_method); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_succeed(t, url + '#fragment').then(text => { + assert_equals(text, blob_contents); + }); + }, fetch_method + ' with a fragment should succeed'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + URL.revokeObjectURL(url); + + return fetch_should_fail(t, url); + }, fetch_method + ' of a revoked URL should fail'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + URL.revokeObjectURL(url + '#fragment'); + + return fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + }, 'Only exact matches should revoke URLs, using ' + fetch_method); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_fail(t, url + '?querystring'); + }, 'Appending a query string should cause ' + fetch_method + ' to fail'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_fail(t, url + '/path'); + }, 'Appending a path should cause ' + fetch_method + ' to fail'); + + for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) { + const url = URL.createObjectURL(blob); + + promise_test(t => { + return fetch_should_fail(t, url, method); + }, fetch_method + ' with method "' + method + '" should fail'); + } +} \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html new file mode 100644 index 00000000000000..adf5a014a668d6 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js new file mode 100644 index 00000000000000..c3e05b64b1a6c8 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js @@ -0,0 +1,9 @@ +self.addEventListener('message', e => { + URL.revokeObjectURL(e.data.url); + // Registering a new object URL will make absolutely sure that the revocation + // has propagated. Without this at least in chrome it is possible for the + // below postMessage to arrive at its destination before the revocation has + // been fully processed. + URL.createObjectURL(new Blob([])); + self.postMessage('revoked'); +}); diff --git a/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html b/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html new file mode 100644 index 00000000000000..a52939a3eb297c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html @@ -0,0 +1,32 @@ + + +FileAPI Test: Verify behavior of Blob URL in unique origins + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html b/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html new file mode 100644 index 00000000000000..2c4921c0344998 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html @@ -0,0 +1,23 @@ + + +FileAPI Test: Verify origin of Blob URL + + + + diff --git a/test/fixtures/wpt/FileAPI/url/url-charset.window.js b/test/fixtures/wpt/FileAPI/url/url-charset.window.js new file mode 100644 index 00000000000000..777709b64a50e5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-charset.window.js @@ -0,0 +1,34 @@ +async_test(t => { + // This could be detected as ISO-2022-JP, in which case there would be no + // bbb` + ], + {type: 'text/html;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + t.add_cleanup(() => { + win.close(); + }); + + win.onload = t.step_func_done(() => { + assert_equals(win.document.charset, 'UTF-8'); + }); +}, 'Blob charset should override any auto-detected charset.'); + +async_test(t => { + const blob = new Blob( + [`\n`], + {type: 'text/html;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + t.add_cleanup(() => { + win.close(); + }); + + win.onload = t.step_func_done(() => { + assert_equals(win.document.charset, 'UTF-8'); + }); +}, 'Blob charset should override .'); diff --git a/test/fixtures/wpt/FileAPI/url/url-format.any.js b/test/fixtures/wpt/FileAPI/url/url-format.any.js new file mode 100644 index 00000000000000..33732fa61fc3dd --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-format.any.js @@ -0,0 +1,64 @@ +// META: timeout=long +const blob = new Blob(['test']); +const file = new File(['test'], 'name'); + +test(() => { + const url_count = 5000; + let list = []; + + for (let i = 0; i < url_count; ++i) + list.push(URL.createObjectURL(blob)); + + list.sort(); + + for (let i = 1; i < list.length; ++i) + assert_not_equals(list[i], list[i-1], 'generated Blob URLs should be unique'); +}, 'Generated Blob URLs are unique'); + +test(() => { + const url = URL.createObjectURL(blob); + assert_equals(typeof url, 'string'); + assert_true(url.startsWith('blob:')); +}, 'Blob URL starts with "blob:"'); + +test(() => { + const url = URL.createObjectURL(file); + assert_equals(typeof url, 'string'); + assert_true(url.startsWith('blob:')); +}, 'Blob URL starts with "blob:" for Files'); + +test(() => { + const url = URL.createObjectURL(blob); + assert_equals(new URL(url).origin, location.origin); + if (location.origin !== 'null') { + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Origin of Blob URL matches our origin'); + +test(() => { + const url = URL.createObjectURL(blob); + const url_record = new URL(url); + assert_equals(url_record.protocol, 'blob:'); + assert_equals(url_record.origin, location.origin); + assert_equals(url_record.host, '', 'host should be an empty string'); + assert_equals(url_record.port, '', 'port should be an empty string'); + const uuid_path_re = /\/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert_true(uuid_path_re.test(url_record.pathname), 'Path must end with a valid UUID'); + if (location.origin !== 'null') { + const nested_url = new URL(url_record.pathname); + assert_equals(nested_url.origin, location.origin); + assert_equals(nested_url.pathname.search(uuid_path_re), 0, 'Path must be a valid UUID'); + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Blob URL parses correctly'); + +test(() => { + const url = URL.createObjectURL(file); + assert_equals(new URL(url).origin, location.origin); + if (location.origin !== 'null') { + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Origin of Blob URL matches our origin for Files'); diff --git a/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js b/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js new file mode 100644 index 00000000000000..1cdad79f7e34e0 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js @@ -0,0 +1,115 @@ +// META: timeout=long +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + URL.revokeObjectURL(url); + + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); +}, 'Fetching a blob URL immediately before revoking it works in an iframe.'); + +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', '/common/blank.html'); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func(() => { + frame.contentWindow.location = url; + URL.revokeObjectURL(url); + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); + }); +}, 'Fetching a blob URL immediately before revoking it works in an iframe navigation.'); + +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + URL.revokeObjectURL(url); + add_completion_callback(() => { win.close(); }); + + win.onload = t.step_func_done(() => { + assert_equals(win.test_result, run_result); + }); +}, 'Opening a blob URL in a new window immediately before revoking it works.'); + +function receive_message_on_channel(t, channel_name) { + const channel = new BroadcastChannel(channel_name); + return new Promise(resolve => { + channel.addEventListener('message', t.step_func(e => { + resolve(e.data); + })); + }); +} + +function window_contents_for_channel(channel_name) { + return '\n' + + ''; +} + +async_test(t => { + const channel_name = 'noopener-window-test'; + const blob = new Blob([window_contents_for_channel(channel_name)], {type: 'text/html'}); + receive_message_on_channel(t, channel_name).then(t.step_func_done(t => { + assert_equals(t, 'foobar'); + })); + const url = URL.createObjectURL(blob); + const win = window.open(); + win.opener = null; + win.location = url; + URL.revokeObjectURL(url); +}, 'Opening a blob URL in a noopener about:blank window immediately before revoking it works.'); + +async_test(t => { + const run_result = 'test_script_OK'; + const blob_contents = 'window.script_test_result = "' + run_result + '";'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + + const e = document.createElement('script'); + e.setAttribute('src', url); + e.onload = t.step_func_done(() => { + assert_equals(window.script_test_result, run_result); + }); + + document.body.appendChild(e); + URL.revokeObjectURL(url); +}, 'Fetching a blob URL immediately before revoking it works in '; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); +}, 'Blob URLs can be used in iframes, and are treated same origin'); + +async_test(t => { + const blob_contents = '\n\n' + + '\n' + + '\n' + + '
\n' + + '
'; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url + '#block2'); + document.body.appendChild(frame); + frame.contentWindow.onscroll = t.step_func_done(() => { + assert_equals(frame.contentWindow.scrollY, 5000); + }); +}, 'Blob URL fragment is implemented.'); diff --git a/test/fixtures/wpt/FileAPI/url/url-lifetime.html b/test/fixtures/wpt/FileAPI/url/url-lifetime.html new file mode 100644 index 00000000000000..ad5d667193a3d0 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-lifetime.html @@ -0,0 +1,56 @@ + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/url-reload.window.js b/test/fixtures/wpt/FileAPI/url/url-reload.window.js new file mode 100644 index 00000000000000..d333b3a74aa82c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-reload.window.js @@ -0,0 +1,36 @@ +function blob_url_reload_test(t, revoke_before_reload) { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func(() => { + if (revoke_before_reload) + URL.revokeObjectURL(url); + assert_equals(frame.contentWindow.test_result, run_result); + frame.contentWindow.test_result = null; + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); + // Slight delay before reloading to ensure revoke actually has had a chance + // to be processed. + t.step_timeout(() => { + frame.contentWindow.location.reload(); + }, 250); + }); +} + +async_test(t => { + blob_url_reload_test(t, false); +}, 'Reloading a blob URL succeeds.'); + + +async_test(t => { + blob_url_reload_test(t, true); +}, 'Reloading a blob URL succeeds even if the URL was revoked.'); diff --git a/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js b/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js new file mode 100644 index 00000000000000..9bd8d383df4e1e --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js @@ -0,0 +1,53 @@ +// META: script=resources/fetch-tests.js + +function fetch_should_succeed(test, request) { + return fetch(request).then(response => response.text()); +} + +function fetch_should_fail(test, url, method = 'GET') { + return promise_rejects_js(test, TypeError, fetch(url, {method: method})); +} + +fetch_tests('fetch', fetch_should_succeed, fetch_should_fail); + +promise_test(t => { + const blob_contents = 'test blob contents'; + const blob_type = 'image/png'; + const blob = new Blob([blob_contents], {type: blob_type}); + const url = URL.createObjectURL(blob); + + return fetch(url).then(response => { + assert_equals(response.headers.get('Content-Type'), blob_type); + }); +}, 'fetch should return Content-Type from Blob'); + +promise_test(t => { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + const request = new Request(url); + + // Revoke the object URL. Request should take a reference to the blob as + // soon as it receives it in open(), so the request succeeds even though we + // revoke the URL before calling fetch(). + URL.revokeObjectURL(url); + + return fetch_should_succeed(t, request).then(text => { + assert_equals(text, blob_contents); + }); +}, 'Revoke blob URL after creating Request, will fetch'); + +promise_test(function(t) { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + + const result = fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + + // Revoke the object URL. fetch should have already resolved the blob URL. + URL.revokeObjectURL(url); + + return result; +}, 'Revoke blob URL after calling fetch, fetch should succeed'); diff --git a/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js b/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js new file mode 100644 index 00000000000000..29d83080ab5845 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js @@ -0,0 +1,68 @@ +// META: script=resources/fetch-tests.js + +function xhr_should_succeed(test, url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = test.step_func(() => { + assert_equals(xhr.status, 200); + assert_equals(xhr.statusText, 'OK'); + resolve(xhr.response); + }); + xhr.onerror = () => reject('Got unexpected error event'); + xhr.send(); + }); +} + +function xhr_should_fail(test, url, method = 'GET') { + const xhr = new XMLHttpRequest(); + xhr.open(method, url); + const result1 = new Promise((resolve, reject) => { + xhr.onload = () => reject('Got unexpected load event'); + xhr.onerror = resolve; + }); + const result2 = new Promise(resolve => { + xhr.onreadystatechange = test.step_func(() => { + if (xhr.readyState !== xhr.DONE) return; + assert_equals(xhr.status, 0); + resolve(); + }); + }); + xhr.send(); + return Promise.all([result1, result2]); +} + +fetch_tests('XHR', xhr_should_succeed, xhr_should_fail); + +async_test(t => { + const blob_contents = 'test blob contents'; + const blob_type = 'image/png'; + const blob = new Blob([blob_contents], {type: blob_type}); + const url = URL.createObjectURL(blob); + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onloadend = t.step_func_done(() => { + assert_equals(xhr.getResponseHeader('Content-Type'), blob_type); + }); + xhr.send(); +}, 'XHR should return Content-Type from Blob'); + +async_test(t => { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + + // Revoke the object URL. XHR should take a reference to the blob as soon as + // it receives it in open(), so the request succeeds even though we revoke the + // URL before calling send(). + URL.revokeObjectURL(url); + + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.response, blob_contents); + }); + xhr.onerror = t.unreached_func('Got unexpected error event'); + + xhr.send(); +}, 'Revoke blob URL after open(), will fetch'); diff --git a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html new file mode 100644 index 00000000000000..7ae32512e07c76 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html @@ -0,0 +1,45 @@ + + +FileAPI Test: Creating Blob URL with File + + + + + + +
+

Test steps:

+
    +
  1. Download blue96x96.png to local.
  2. +
  3. Select the local file (blue96x96.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html new file mode 100644 index 00000000000000..534c1de9968da8 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html @@ -0,0 +1,28 @@ + + +FileAPI Test: Creating Blob URL with File as image source + + + +
+

Test steps:

+
    +
  1. Download blue96x96.png to local.
  2. +
  3. Select the local file (blue96x96.png) to run the test.
  4. +
+

Pass/fail criteria:

+

Test passes if there is a filled blue square.

+ +

+

+
+ + + diff --git a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html new file mode 100644 index 00000000000000..7d7390442d3631 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html @@ -0,0 +1,12 @@ + + +FileAPI Reference File + + + +

Test passes if there is a filled blue square.

+ +

+ +

+ diff --git a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html new file mode 100644 index 00000000000000..468dcb086d770a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html @@ -0,0 +1,27 @@ + + + +FileAPI Test: Creating Blob URL via XMLHttpRequest as image source + + + + +

Test passes if there is a filled blue square.

+ +

+ +

+ + + + diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index fc609bfca90b5a..4a5d73c742acea 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -20,6 +20,7 @@ Last update: - hr-time: https://github.com/web-platform-tests/wpt/tree/a5d1774ecf/hr-time - common: https://github.com/web-platform-tests/wpt/tree/4dacb6e2ff/common - dom/abort: https://github.com/web-platform-tests/wpt/tree/7caa3de747/dom/abort +- FileAPI: https://github.com/web-platform-tests/wpt/tree/d9d921b8f9/FileAPI [Web Platform Tests]: https://github.com/web-platform-tests/wpt [`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/master/docs/git-node.md#git-node-wpt diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 3cbb5f8f99bbdb..20fed894db94b6 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -38,5 +38,9 @@ "dom/abort": { "commit": "7caa3de7471cf19b78ee9efa313c7341a462b5e3", "path": "dom/abort" + }, + "FileAPI": { + "commit": "d9d921b8f9235e0d2ec92672040c0ccfc8262e21", + "path": "FileAPI" } } \ No newline at end of file diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js new file mode 100644 index 00000000000000..795d4bb6fbdff5 --- /dev/null +++ b/test/parallel/test-blob.js @@ -0,0 +1,186 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Blob } = require('buffer'); + +{ + const b = new Blob(); + assert.strictEqual(b.size, 0); + assert.strictEqual(b.type, ''); +} + +assert.throws(() => new Blob(false), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob('hello'), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob({}), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +assert.throws(() => new Blob(['test', 1]), { + code: 'ERR_INVALID_ARG_TYPE' +}); + +{ + const b = new Blob([]); + assert(b); + assert.strictEqual(b.size, 0); + assert.strictEqual(b.type, ''); + + b.arrayBuffer().then(common.mustCall((ab) => { + assert.deepStrictEqual(ab, new ArrayBuffer(0)); + })); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, ''); + })); + const c = b.slice(); + assert.strictEqual(c.size, 0); +} + +{ + assert.throws(() => new Blob([], { type: 1 }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => new Blob([], { type: false }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => new Blob([], { type: {} }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + const b = new Blob(['616263'], { encoding: 'hex', type: 'foo' }); + assert.strictEqual(b.size, 3); + assert.strictEqual(b.type, 'foo'); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob([Buffer.from('abc')]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob([new ArrayBuffer(3)]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, '\0\0\0'); + })); +} + +{ + const b = new Blob([new Uint8Array(3)]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, '\0\0\0'); + })); +} + +{ + const b = new Blob([new Blob(['abc'])]); + assert.strictEqual(b.size, 3); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'abc'); + })); +} + +{ + const b = new Blob(['hello', Buffer.from('world')]); + assert.strictEqual(b.size, 10); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob( + [ + 'h', + 'e', + 'l', + 'lo', + Buffer.from('world') + ]); + assert.strictEqual(b.size, 10); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob(['hello', Buffer.from('world')]); + assert.strictEqual(b.size, 10); + assert.strictEqual(b.type, ''); + + const c = b.slice(1, -1, 'foo'); + assert.strictEqual(c.type, 'foo'); + c.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'elloworl'); + })); + + const d = c.slice(1, -1); + d.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'llowor'); + })); + + const e = d.slice(1, -1); + e.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'lowo'); + })); + + const f = e.slice(1, -1); + f.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'ow'); + })); + + const g = f.slice(1, -1); + assert.strictEqual(g.type, 'foo'); + g.text().then(common.mustCall((text) => { + assert.strictEqual(text, ''); + })); + + assert.strictEqual(b.size, 10); + assert.strictEqual(b.type, ''); + + assert.throws(() => b.slice(-1, 1), { + code: 'ERR_OUT_OF_RANGE' + }); + assert.throws(() => b.slice(1, 100), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws(() => b.slice(1, 2, false), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + const b = new Blob([Buffer.from('hello'), Buffer.from('world')]); + const mc = new MessageChannel(); + mc.port1.onmessage = common.mustCall(({ data }) => { + data.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); + mc.port1.close(); + }); + mc.port2.postMessage(b); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'helloworld'); + })); +} + +{ + const b = new Blob(['hello'], { type: '\x01' }); + assert.strictEqual(b.type, ''); +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 0887b8a48362f6..105bfb10866499 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -101,6 +101,7 @@ const expectedModules = new Set([ 'NativeModule internal/vm/module', 'NativeModule internal/worker/io', 'NativeModule internal/worker/js_transferable', + 'NativeModule internal/blob', 'NativeModule path', 'NativeModule stream', 'NativeModule timers', diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index e37e613b747dc2..e9b2e2d2cca247 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -69,6 +69,7 @@ const { getSystemErrorName } = require('util'); delete providers.ELDHISTOGRAM; delete providers.SIGINTWATCHDOG; delete providers.WORKERHEAPSNAPSHOT; + delete providers.FIXEDSIZEBLOBCOPY; const objKeys = Object.keys(providers); if (objKeys.length > 0) diff --git a/test/wpt/status/FileAPI/blob.json b/test/wpt/status/FileAPI/blob.json new file mode 100644 index 00000000000000..1b463a1f0a1b7f --- /dev/null +++ b/test/wpt/status/FileAPI/blob.json @@ -0,0 +1,8 @@ +{ + "Blob-stream.any.js": { + "skip": "Depends on Web Streams API" + }, + "Blob-in-worker.worker.js": { + "skip": "Depends on Web Workers API" + } +} diff --git a/test/wpt/test-blob.js b/test/wpt/test-blob.js new file mode 100644 index 00000000000000..92e18bc0ef2f22 --- /dev/null +++ b/test/wpt/test-blob.js @@ -0,0 +1,13 @@ +'use strict'; + +require('../common'); +const { WPTRunner } = require('../common/wpt'); + +const runner = new WPTRunner('FileAPI/blob'); + +runner.setInitScript(` + const { Blob } = require('buffer'); + global.Blob = Blob; +`); + +runner.runJsTests(); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 624ff0974d9cb5..5a91b1556c596d 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -40,6 +40,8 @@ const customTypesMap = { 'WebAssembly.Instance': `${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`, + 'Blob': 'buffer.html#buffer_class_blob', + 'BroadcastChannel': 'worker_threads.html#worker_threads_class_broadcastchannel_' + 'extends_eventtarget',