diff --git a/doc/api/buffer.md b/doc/api/buffer.md index c1423229ca6a34..3f220562d625ed 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. @@ -3388,6 +3501,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 b16d696b023fe1..bf9ac48fbce1ed 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -116,6 +116,10 @@ const { addBufferPrototypeMethods } = require('internal/buffer'); +const { + Blob, +} = require('internal/blob'); + FastBuffer.prototype.constructor = Buffer; Buffer.prototype = FastBuffer.prototype; addBufferPrototypeMethods(Buffer.prototype); @@ -1240,6 +1244,7 @@ function atob(input) { } module.exports = { + Blob, Buffer, SlowBuffer, transcode, diff --git a/lib/child_process.js b/lib/child_process.js index 09369316de52f1..8c10ce36cc7d74 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -28,12 +28,12 @@ const { ObjectAssign, ObjectDefineProperty, ObjectPrototypeHasOwnProperty, - Promise, } = primordials; const { promisify, convertToValidSignal, + createDeferredPromise, getSystemErrorName } = require('internal/util'); const { isArrayBufferView } = require('internal/util/types'); @@ -215,12 +215,7 @@ function exec(command, options, callback) { const customPromiseExecFunction = (orig) => { return (...args) => { - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); + const { promise, resolve, reject } = createDeferredPromise(); promise.child = orig(...args, (err, stdout, stderr) => { if (err !== null) { diff --git a/lib/internal/blob.js b/lib/internal/blob.js new file mode 100644 index 00000000000000..927b9f54046bf2 --- /dev/null +++ b/lib/internal/blob.js @@ -0,0 +1,236 @@ +'use strict'; + +const { + ArrayFrom, + MathMax, + MathMin, + ObjectDefineProperty, + ObjectSetPrototypeOf, + PromiseResolve, + RegExpPrototypeTest, + StringPrototypeToLowerCase, + Symbol, + SymbolIterator, + SymbolToStringTag, + Uint8Array, +} = primordials; + +const { + createBlob, + FixedSizeBlobCopyJob, +} = internalBinding('buffer'); + +const { TextDecoder } = require('internal/encoding'); + +const { + JSTransferable, + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); + +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); + +const { + createDeferredPromise, + customInspectSymbol: kInspect, + emitExperimentalWarning, +} = require('internal/util'); +const { inspect } = require('internal/util/inspect'); + +const { + AbortError, + codes: { + ERR_INVALID_ARG_TYPE, + ERR_BUFFER_TOO_LARGE, + } +} = require('internal/errors'); + +const { + validateObject, + isUint32, +} = require('internal/validators'); + +const kHandle = Symbol('kHandle'); +const kType = Symbol('kType'); +const kLength = Symbol('kLength'); + +const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u; + +let Buffer; + +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 (isAnyArrayBuffer(source)) { + source = new Uint8Array(source); + } else if (!isArrayBufferView(source)) { + source = lazyBuffer().from(`${source}`, encoding); + } + + // 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); + } + validateObject(options, 'options'); + const { encoding = 'utf8' } = options; + let { type = '' } = options; + + let length = 0; + const sources_ = ArrayFrom(sources, (source) => { + const { 0: len, 1: src } = getSource(source, encoding); + length += len; + return src; + }); + + if (!isUint32(length)) + throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF); + + super(); + this[kHandle] = createBlob(sources_, length); + this[kLength] = length; + + type = `${type}`; + this[kType] = RegExpPrototypeTest(disallowedTypeCharacters, 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], contentType = '') { + if (start < 0) { + start = MathMax(this[kLength] + start, 0); + } else { + start = MathMin(start, this[kLength]); + } + start |= 0; + + if (end < 0) { + end = MathMax(this[kLength] + end, 0); + } else { + end = MathMin(end, this[kLength]); + } + end |= 0; + + contentType = `${contentType}`; + if (RegExpPrototypeTest(disallowedTypeCharacters, contentType)) { + contentType = ''; + } else { + contentType = StringPrototypeToLowerCase(contentType); + } + + const span = MathMax(end - start, 0); + + return new InternalBlob( + this[kHandle].slice(start, start + span), span, contentType); + } + + async arrayBuffer() { + const job = new FixedSizeBlobCopyJob(this[kHandle]); + + const ret = job.run(); + if (ret !== undefined) + return PromiseResolve(ret); + + const { + promise, + resolve, + reject + } = createDeferredPromise(); + 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()); + } +} + +ObjectDefineProperty(Blob.prototype, SymbolToStringTag, { + configurable: true, + value: 'Blob', +}); + +InternalBlob.prototype.constructor = Blob; +ObjectSetPrototypeOf( + InternalBlob.prototype, + Blob.prototype); + +module.exports = { + Blob, + InternalBlob, + isBlob, +}; diff --git a/lib/internal/util.js b/lib/internal/util.js index c28f51e40c9f61..ea507c3f8f261e 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -415,11 +415,23 @@ function sleep(msec) { _sleep(msec); } +function createDeferredPromise() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve, reject }; +} + module.exports = { assertCrypto, cachedResult, convertToValidSignal, createClassWrapper, + createDeferredPromise, decorateErrorStack, deprecate, emitExperimentalWarning, diff --git a/node.gyp b/node.gyp index 5164a4915ba992..2197f0fd755a15 100644 --- a/node.gyp +++ b/node.gyp @@ -101,6 +101,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', @@ -589,6 +590,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', @@ -687,6 +689,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 81d0db01c950ec..374228c3fe8866 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 10c4e0fbf72958..a6fbddefd2dbe4 100644 --- a/src/env.h +++ b/src/env.h @@ -413,6 +413,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_constructor_template, v8::FunctionTemplate) \ 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..aaea61fb65357c --- /dev/null +++ b/src/node_blob.cc @@ -0,0 +1,324 @@ +#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 "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( + BaseObject::kInternalFieldCount); + 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() { + 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()); + job->destination_ = + ArrayBuffer::NewBackingStore(env->isolate(), job->length_); + if (job->mode() == FixedSizeBlobCopyJob::Mode::ASYNC) + return job->ScheduleWork(); + + job->DoThreadPoolWork(); + args.GetReturnValue().Set( + ArrayBuffer::New(env->isolate(), job->destination_)); +} + +} // namespace node diff --git a/src/node_blob.h b/src/node_blob.h new file mode 100644 index 00000000000000..c54ed2f2920385 --- /dev/null +++ b/src/node_blob.h @@ -0,0 +1,133 @@ +#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 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 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 8781c02b2f6e70..1fedfad9c99b00 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_internals.h" @@ -1175,6 +1176,8 @@ void Initialize(Local target, env->SetMethod(target, "ucs2Write", StringWrite); env->SetMethod(target, "utf8Write", StringWrite); + Blob::Initialize(env, target); + // It can be a nullptr when running inside an isolate where we // do not own the ArrayBuffer allocator. if (NodeArrayBufferAllocator* allocator = diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index e507bafc8c523f..abd28d3e4aa7c5 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -172,8 +172,8 @@ class SocketAddressBase : public BaseObject { } void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(SocketAddressBase); - SET_SELF_SIZE(SocketAddressBase); + SET_MEMORY_INFO_NAME(SocketAddressBase) + SET_SELF_SIZE(SocketAddressBase) TransferMode GetTransferMode() const override { return TransferMode::kCloneable; 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-dom.window.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-dom.window.js new file mode 100644 index 00000000000000..4fd4a43ec4bea7 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-dom.window.js @@ -0,0 +1,53 @@ +// META: title=Blob constructor +// META: script=../support/Blob.js +'use strict'; + +var test_error = { + name: "test", + message: "test error", +}; + +test(function() { + var args = [ + document.createElement("div"), + window, + ]; + args.forEach(function(arg) { + assert_throws_js(TypeError, function() { + new Blob(arg); + }, "Should throw for argument " + format_value(arg) + "."); + }); +}, "Passing platform objects for blobParts should throw a TypeError."); + +test(function() { + var element = document.createElement("div"); + element.appendChild(document.createElement("div")); + element.appendChild(document.createElement("p")); + var list = element.children; + Object.defineProperty(list, "length", { + get: function() { throw test_error; } + }); + assert_throws_exactly(test_error, function() { + new Blob(list); + }); +}, "A platform object that supports indexed properties should be treated as a sequence for the blobParts argument (overwritten 'length'.)"); + +test_blob(function() { + var select = document.createElement("select"); + select.appendChild(document.createElement("option")); + return new Blob(select); +}, { + expected: "[object HTMLOptionElement]", + type: "", + desc: "Passing an platform object that supports indexed properties as the blobParts array should work (select)." +}); + +test_blob(function() { + var elm = document.createElement("div"); + elm.setAttribute("foo", "bar"); + return new Blob(elm.attributes); +}, { + expected: "[object Attr]", + type: "", + desc: "Passing an platform object that supports indexed properties as the blobParts array should work (attributes)." +}); \ No newline at end of file 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.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js new file mode 100644 index 00000000000000..6c34d7e34b93f9 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js @@ -0,0 +1,459 @@ +// META: title=Blob constructor +// META: script=../support/Blob.js +'use strict'; + +test(function() { + assert_true("Blob" in globalThis, "globalThis should have a Blob property."); + assert_equals(Blob.length, 0, "Blob.length should be 0."); + assert_true(Blob instanceof Function, "Blob should be a function."); +}, "Blob interface object"); + +// Step 1. +test(function() { + var blob = new Blob(); + assert_true(blob instanceof Blob); + assert_equals(String(blob), '[object Blob]'); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor with no arguments"); +test(function() { + assert_throws_js(TypeError, function() { var blob = Blob(); }); +}, "Blob constructor with no arguments, without 'new'"); +test(function() { + var blob = new Blob; + assert_true(blob instanceof Blob); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor without brackets"); +test(function() { + var blob = new Blob(undefined); + assert_true(blob instanceof Blob); + assert_equals(String(blob), '[object Blob]'); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor with undefined as first argument"); + +// blobParts argument (WebIDL). +test(function() { + var args = [ + null, + true, + false, + 0, + 1, + 1.5, + "FAIL", + new Date(), + new RegExp(), + {}, + { 0: "FAIL", length: 1 }, + ]; + args.forEach(function(arg) { + assert_throws_js(TypeError, function() { + new Blob(arg); + }, "Should throw for argument " + format_value(arg) + "."); + }); +}, "Passing non-objects, Dates and RegExps for blobParts should throw a TypeError."); + +test_blob(function() { + return new Blob({ + [Symbol.iterator]: Array.prototype[Symbol.iterator], + }); +}, { + expected: "", + type: "", + desc: "A plain object with @@iterator should be treated as a sequence for the blobParts argument." +}); +test(t => { + const blob = new Blob({ + [Symbol.iterator]() { + var i = 0; + return {next: () => [ + {done:false, value:'ab'}, + {done:false, value:'cde'}, + {done:true} + ][i++] + }; + } + }); + assert_equals(blob.size, 5, 'Custom @@iterator should be treated as a sequence'); +}, "A plain object with custom @@iterator should be treated as a sequence for the blobParts argument."); +test_blob(function() { + return new Blob({ + [Symbol.iterator]: Array.prototype[Symbol.iterator], + 0: "PASS", + length: 1 + }); +}, { + expected: "PASS", + type: "", + desc: "A plain object with @@iterator and a length property should be treated as a sequence for the blobParts argument." +}); +test_blob(function() { + return new Blob(new String("xyz")); +}, { + expected: "xyz", + type: "", + desc: "A String object should be treated as a sequence for the blobParts argument." +}); +test_blob(function() { + return new Blob(new Uint8Array([1, 2, 3])); +}, { + expected: "123", + type: "", + desc: "A Uint8Array object should be treated as a sequence for the blobParts argument." +}); + +var test_error = { + name: "test", + message: "test error", +}; + +test(function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + get length() { throw test_error; } + }; + assert_throws_exactly(test_error, function() { + new Blob(obj); + }); +}, "The length getter should be invoked and any exceptions should be propagated."); + +test(function() { + assert_throws_exactly(test_error, function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + length: { + valueOf: null, + toString: function() { throw test_error; } + } + }; + new Blob(obj); + }); + assert_throws_exactly(test_error, function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + length: { valueOf: function() { throw test_error; } } + }; + new Blob(obj); + }); +}, "ToUint32 should be applied to the length and any exceptions should be propagated."); + +test(function() { + var received = []; + var obj = { + get [Symbol.iterator]() { + received.push("Symbol.iterator"); + return Array.prototype[Symbol.iterator]; + }, + get length() { + received.push("length getter"); + return { + valueOf: function() { + received.push("length valueOf"); + return 3; + } + }; + }, + get 0() { + received.push("0 getter"); + return { + toString: function() { + received.push("0 toString"); + return "a"; + } + }; + }, + get 1() { + received.push("1 getter"); + throw test_error; + }, + get 2() { + received.push("2 getter"); + assert_unreached("Should not call the getter for 2 if the getter for 1 threw."); + } + }; + assert_throws_exactly(test_error, function() { + new Blob(obj); + }); + assert_array_equals(received, [ + "Symbol.iterator", + "length getter", + "length valueOf", + "0 getter", + "0 toString", + "length getter", + "length valueOf", + "1 getter", + ]); +}, "Getters and value conversions should happen in order until an exception is thrown."); + +// XXX should add tests edge cases of ToLength(length) + +test(function() { + assert_throws_exactly(test_error, function() { + new Blob([{ toString: function() { throw test_error; } }]); + }, "Throwing toString"); + assert_throws_exactly(test_error, function() { + new Blob([{ toString: undefined, valueOf: function() { throw test_error; } }]); + }, "Throwing valueOf"); + assert_throws_exactly(test_error, function() { + new Blob([{ + toString: function() { throw test_error; }, + valueOf: function() { assert_unreached("Should not call valueOf if toString is present."); } + }]); + }, "Throwing toString and valueOf"); + assert_throws_js(TypeError, function() { + new Blob([{toString: null, valueOf: null}]); + }, "Null toString and valueOf"); +}, "ToString should be called on elements of the blobParts array and any exceptions should be propagated."); + +test_blob(function() { + var arr = [ + { toString: function() { arr.pop(); return "PASS"; } }, + { toString: function() { assert_unreached("Should have removed the second element of the array rather than called toString() on it."); } } + ]; + return new Blob(arr); +}, { + expected: "PASS", + type: "", + desc: "Changes to the blobParts array should be reflected in the returned Blob (pop)." +}); + +test_blob(function() { + var arr = [ + { + toString: function() { + if (arr.length === 3) { + return "A"; + } + arr.unshift({ + toString: function() { + assert_unreached("Should only access index 0 once."); + } + }); + return "P"; + } + }, + { + toString: function() { + return "SS"; + } + } + ]; + return new Blob(arr); +}, { + expected: "PASS", + type: "", + desc: "Changes to the blobParts array should be reflected in the returned Blob (unshift)." +}); + +test_blob(function() { + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17652 + return new Blob([ + null, + undefined, + true, + false, + 0, + 1, + new String("stringobject"), + [], + ['x', 'y'], + {}, + { 0: "FAIL", length: 1 }, + { toString: function() { return "stringA"; } }, + { toString: undefined, valueOf: function() { return "stringB"; } }, + { valueOf: function() { assert_unreached("Should not call valueOf if toString is present on the prototype."); } } + ]); +}, { + expected: "nullundefinedtruefalse01stringobjectx,y[object Object][object Object]stringAstringB[object Object]", + type: "", + desc: "ToString should be called on elements of the blobParts array." +}); + +test_blob(function() { + return new Blob([ + new ArrayBuffer(8) + ]); +}, { + expected: "\0\0\0\0\0\0\0\0", + type: "", + desc: "ArrayBuffer elements of the blobParts array should be supported." +}); + +test_blob(function() { + return new Blob([ + new Uint8Array([0x50, 0x41, 0x53, 0x53]), + new Int8Array([0x50, 0x41, 0x53, 0x53]), + new Uint16Array([0x4150, 0x5353]), + new Int16Array([0x4150, 0x5353]), + new Uint32Array([0x53534150]), + new Int32Array([0x53534150]), + new Float32Array([0xD341500000]) + ]); +}, { + expected: "PASSPASSPASSPASSPASSPASSPASS", + type: "", + desc: "Passing typed arrays as elements of the blobParts array should work." +}); +test_blob(function() { + return new Blob([ + // 0x535 3415053534150 + // 0x535 = 0b010100110101 -> Sign = +, Exponent = 1333 - 1023 = 310 + // 0x13415053534150 * 2**(-52) + // ==> 0x13415053534150 * 2**258 = 2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680 + new Float64Array([2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680]) + ]); +}, { + expected: "PASSPASS", + type: "", + desc: "Passing a Float64Array as element of the blobParts array should work." +}); + + + +var t_ports = async_test("Passing a FrozenArray as the blobParts array should work (FrozenArray)."); +t_ports.step(function() { + var channel = new MessageChannel(); + channel.port2.onmessage = this.step_func(function(e) { + var b_ports = new Blob(e.ports); + assert_equals(b_ports.size, "[object MessagePort]".length); + this.done(); + }); + var channel2 = new MessageChannel(); + channel.port1.postMessage('', [channel2.port1]); +}); + +test_blob(function() { + var blob = new Blob(['foo']); + return new Blob([blob, blob]); +}, { + expected: "foofoo", + type: "", + desc: "Array with two blobs" +}); + +test_blob_binary(function() { + var view = new Uint8Array([0, 255, 0]); + return new Blob([view.buffer, view.buffer]); +}, { + expected: [0, 255, 0, 0, 255, 0], + type: "", + desc: "Array with two buffers" +}); + +test_blob_binary(function() { + var view = new Uint8Array([0, 255, 0, 4]); + var blob = new Blob([view, view]); + assert_equals(blob.size, 8); + var view1 = new Uint16Array(view.buffer, 2); + return new Blob([view1, view.buffer, view1]); +}, { + expected: [0, 4, 0, 255, 0, 4, 0, 4], + type: "", + desc: "Array with two bufferviews" +}); + +test_blob(function() { + var view = new Uint8Array([0]); + var blob = new Blob(["fo"]); + return new Blob([view.buffer, blob, "foo"]); +}, { + expected: "\0fofoo", + type: "", + desc: "Array with mixed types" +}); + +test(function() { + const accessed = []; + const stringified = []; + + new Blob([], { + get type() { accessed.push('type'); }, + get endings() { accessed.push('endings'); } + }); + new Blob([], { + type: { toString: () => { stringified.push('type'); return ''; } }, + endings: { toString: () => { stringified.push('endings'); return 'transparent'; } } + }); + assert_array_equals(accessed, ['endings', 'type']); + assert_array_equals(stringified, ['endings', 'type']); +}, "options properties should be accessed in lexicographic order."); + +test(function() { + assert_throws_exactly(test_error, function() { + new Blob( + [{ toString: function() { throw test_error } }], + { + get type() { assert_unreached("type getter should not be called."); } + } + ); + }); +}, "Arguments should be evaluated from left to right."); + +[ + null, + undefined, + {}, + { unrecognized: true }, + /regex/, + function() {} +].forEach(function(arg, idx) { + test_blob(function() { + return new Blob([], arg); + }, { + expected: "", + type: "", + desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults." + }); + test_blob(function() { + return new Blob(["\na\r\nb\n\rc\r"], arg); + }, { + expected: "\na\r\nb\n\rc\r", + type: "", + desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults (with newlines)." + }); +}); + +[ + 123, + 123.4, + true, + 'abc' +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new Blob([], arg), + 'Blob constructor should throw with invalid property bag'); + }, `Passing ${JSON.stringify(arg)} for options should throw`); +}); + +var type_tests = [ + // blobParts, type, expected type + [[], '', ''], + [[], 'a', 'a'], + [[], 'A', 'a'], + [[], 'text/html', 'text/html'], + [[], 'TEXT/HTML', 'text/html'], + [[], 'text/plain;charset=utf-8', 'text/plain;charset=utf-8'], + [[], '\u00E5', ''], + [[], '\uD801\uDC7E', ''], // U+1047E + [[], ' image/gif ', ' image/gif '], + [[], '\timage/gif\t', ''], + [[], 'image/gif;\u007f', ''], + [[], '\u0130mage/gif', ''], // uppercase i with dot + [[], '\u0131mage/gif', ''], // lowercase dotless i + [[], 'image/gif\u0000', ''], + // check that type isn't changed based on sniffing + [[0x3C, 0x48, 0x54, 0x4D, 0x4C, 0x3E], 'unknown/unknown', 'unknown/unknown'], // "" + [[0x00, 0xFF], 'text/plain', 'text/plain'], + [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 'image/png', 'image/png'], // "GIF89a" +]; + +type_tests.forEach(function(t) { + test(function() { + var arr = new Uint8Array([t[0]]).buffer; + var b = new Blob([arr], {type:t[1]}); + assert_equals(b.type, t[2]); + }, "Blob with type " + format_value(t[1])); +}); 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.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.any.js new file mode 100644 index 00000000000000..388fd9282c94ab --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.any.js @@ -0,0 +1,32 @@ +// META: title=Blob slice overflow +'use strict'; + +var text = ''; + +for (var i = 0; i < 2000; ++i) { + text += 'A'; +} + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(-1, blob.size); + assert_equals(sliceBlob.size, 1, "Blob slice size"); +}, "slice start is negative, relativeStart will be max((size + start), 0)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size + 1, blob.size); + assert_equals(sliceBlob.size, 0, "Blob slice size"); +}, "slice start is greater than blob size, relativeStart will be min(start, size)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size - 2, -1); + assert_equals(sliceBlob.size, 1, "Blob slice size"); +}, "slice end is negative, relativeEnd will be max((size + end), 0)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size - 2, blob.size + 999); + assert_equals(sliceBlob.size, 2, "Blob slice size"); +}, "slice end is greater than blob size, relativeEnd will be min(end, size)"); 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.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-slice.any.js new file mode 100644 index 00000000000000..1f85d44d269191 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-slice.any.js @@ -0,0 +1,231 @@ +// META: title=Blob slice +// META: script=../support/Blob.js +'use strict'; + +test_blob(function() { + var blobTemp = new Blob(["PASS"]); + return blobTemp.slice(); +}, { + expected: "PASS", + type: "", + desc: "no-argument Blob slice" +}); + +test(function() { + var blob1, blob2; + + test_blob(function() { + return blob1 = new Blob(["squiggle"]); + }, { + expected: "squiggle", + type: "", + desc: "blob1." + }); + + test_blob(function() { + return blob2 = new Blob(["steak"], {type: "content/type"}); + }, { + expected: "steak", + type: "content/type", + desc: "blob2." + }); + + test_blob(function() { + return new Blob().slice(0,0,null); + }, { + expected: "", + type: "null", + desc: "null type Blob slice" + }); + + test_blob(function() { + return new Blob().slice(0,0,undefined); + }, { + expected: "", + type: "", + desc: "undefined type Blob slice" + }); + + test_blob(function() { + return new Blob().slice(0,0); + }, { + expected: "", + type: "", + desc: "no type Blob slice" + }); + + var arrayBuffer = new ArrayBuffer(16); + var int8View = new Int8Array(arrayBuffer); + for (var i = 0; i < 16; i++) { + int8View[i] = i + 65; + } + + var testData = [ + [ + ["PASSSTRING"], + [{start: -6, contents: "STRING"}, + {start: -12, contents: "PASSSTRING"}, + {start: 4, contents: "STRING"}, + {start: 12, contents: ""}, + {start: 0, end: -6, contents: "PASS"}, + {start: 0, end: -12, contents: ""}, + {start: 0, end: 4, contents: "PASS"}, + {start: 0, end: 12, contents: "PASSSTRING"}, + {start: 7, end: 4, contents: ""}] + ], + + // Test 3 strings + [ + ["foo", "bar", "baz"], + [{start: 0, end: 9, contents: "foobarbaz"}, + {start: 0, end: 3, contents: "foo"}, + {start: 3, end: 9, contents: "barbaz"}, + {start: 6, end: 9, contents: "baz"}, + {start: 6, end: 12, contents: "baz"}, + {start: 0, end: 9, contents: "foobarbaz"}, + {start: 0, end: 11, contents: "foobarbaz"}, + {start: 10, end: 15, contents: ""}] + ], + + // Test string, Blob, string + [ + ["foo", blob1, "baz"], + [{start: 0, end: 3, contents: "foo"}, + {start: 3, end: 11, contents: "squiggle"}, + {start: 2, end: 4, contents: "os"}, + {start: 10, end: 12, contents: "eb"}] + ], + + // Test blob, string, blob + [ + [blob1, "foo", blob1], + [{start: 0, end: 8, contents: "squiggle"}, + {start: 7, end: 9, contents: "ef"}, + {start: 10, end: 12, contents: "os"}, + {start: 1, end: 4, contents: "qui"}, + {start: 12, end: 15, contents: "qui"}, + {start: 40, end: 60, contents: ""}] + ], + + // Test blobs all the way down + [ + [blob2, blob1, blob2], + [{start: 0, end: 5, contents: "steak"}, + {start: 5, end: 13, contents: "squiggle"}, + {start: 13, end: 18, contents: "steak"}, + {start: 1, end: 3, contents: "te"}, + {start: 6, end: 10, contents: "quig"}] + ], + + // Test an ArrayBufferView + [ + [int8View, blob1, "foo"], + [{start: 0, end: 8, contents: "ABCDEFGH"}, + {start: 8, end: 18, contents: "IJKLMNOPsq"}, + {start: 17, end: 20, contents: "qui"}, + {start: 4, end: 12, contents: "EFGHIJKL"}] + ], + + // Test a partial ArrayBufferView + [ + [new Uint8Array(arrayBuffer, 3, 5), blob1, "foo"], + [{start: 0, end: 8, contents: "DEFGHsqu"}, + {start: 8, end: 18, contents: "igglefoo"}, + {start: 4, end: 12, contents: "Hsquiggl"}] + ], + + // Test type coercion of a number + [ + [3, int8View, "foo"], + [{start: 0, end: 8, contents: "3ABCDEFG"}, + {start: 8, end: 18, contents: "HIJKLMNOPf"}, + {start: 17, end: 21, contents: "foo"}, + {start: 4, end: 12, contents: "DEFGHIJK"}] + ], + + [ + [(new Uint8Array([0, 255, 0])).buffer, + new Blob(['abcd']), + 'efgh', + 'ijklmnopqrstuvwxyz'], + [{start: 1, end: 4, contents: "\uFFFD\u0000a"}, + {start: 4, end: 8, contents: "bcde"}, + {start: 8, end: 12, contents: "fghi"}, + {start: 1, end: 12, contents: "\uFFFD\u0000abcdefghi"}] + ] + ]; + + testData.forEach(function(data, i) { + var blobs = data[0]; + var tests = data[1]; + tests.forEach(function(expectations, j) { + test(function() { + var blob = new Blob(blobs); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + + test_blob(function() { + return expectations.end === undefined + ? blob.slice(expectations.start) + : blob.slice(expectations.start, expectations.end); + }, { + expected: expectations.contents, + type: "", + desc: "Slicing test: slice (" + i + "," + j + ")." + }); + }, "Slicing test (" + i + "," + j + ")."); + }); + }); +}, "Slices"); + +var invalidTypes = [ + "\xFF", + "te\x09xt/plain", + "te\x00xt/plain", + "te\x1Fxt/plain", + "te\x7Fxt/plain" +]; +invalidTypes.forEach(function(type) { + test_blob(function() { + var blob = new Blob(["PASS"]); + return blob.slice(0, 4, type); + }, { + expected: "PASS", + type: "", + desc: "Invalid contentType (" + format_value(type) + ")" + }); +}); + +var validTypes = [ + "te(xt/plain", + "te)xt/plain", + "text/plain", + "te@xt/plain", + "te,xt/plain", + "te;xt/plain", + "te:xt/plain", + "te\\xt/plain", + "te\"xt/plain", + "te/xt/plain", + "te[xt/plain", + "te]xt/plain", + "te?xt/plain", + "te=xt/plain", + "te{xt/plain", + "te}xt/plain", + "te\x20xt/plain", + "TEXT/PLAIN", + "text/plain;charset = UTF-8", + "text/plain;charset=UTF-8" +]; +validTypes.forEach(function(type) { + test_blob(function() { + var blob = new Blob(["PASS"]); + return blob.slice(0, 4, type); + }, { + expected: "PASS", + type: type.toLowerCase(), + desc: "Valid contentType (" + format_value(type) + ")" + }); +}); 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.html b/test/fixtures/wpt/FileAPI/file/send-file-form-controls.html new file mode 100644 index 00000000000000..6347065bcae14b --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-controls.html @@ -0,0 +1,113 @@ + + +Upload files named using controls + + + + + + + + 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.html b/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.html new file mode 100644 index 00000000000000..c931c9be3aba8c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.html @@ -0,0 +1,65 @@ + + + +Upload files in ISO-2022-JP form + + + + + + + + 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.html b/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.html new file mode 100644 index 00000000000000..a6568e2e56e4d5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.html @@ -0,0 +1,226 @@ + + +Upload files named using punctuation + + + + + + + + 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.html b/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.html new file mode 100644 index 00000000000000..21b219ffd2d066 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.html @@ -0,0 +1,62 @@ + + +Upload files in Windows-1252 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.html b/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.html new file mode 100644 index 00000000000000..8d6605d86deb1c --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.html @@ -0,0 +1,63 @@ + + +Upload files in x-user-defined form + + + + + + + + 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 b7ce4b81443080..30dda41a52379b 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -20,6 +20,7 @@ Last update: - html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/ddfe9c089b/html/webappapis/timers - hr-time: https://github.com/web-platform-tests/wpt/tree/a5d1774ecf/hr-time - dom/abort: https://github.com/web-platform-tests/wpt/tree/1728d198c9/dom/abort +- FileAPI: https://github.com/web-platform-tests/wpt/tree/3b279420d4/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 4a3e4b84d1f339..77ace95841fab3 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -38,5 +38,9 @@ "dom/abort": { "commit": "1728d198c92834d92f7f399ef35e7823d5bfa0e4", "path": "dom/abort" + }, + "FileAPI": { + "commit": "3b279420d40afea32506e823f9ac005448f4f3d8", + "path": "FileAPI" } } diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js new file mode 100644 index 00000000000000..b034ed99f82f5e --- /dev/null +++ b/test/parallel/test-blob.js @@ -0,0 +1,201 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Blob } = require('buffer'); +const { inspect } = require('util'); +const { MessageChannel } = require('worker_threads'); + +{ + 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' +}); + +{ + 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.strictEqual(new Blob([], { type: 1 }).type, '1'); + assert.strictEqual(new Blob([], { type: false }).type, 'false'); + assert.strictEqual(new Blob([], { type: {} }).type, '[object object]'); +} + +{ + 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, ''); + g.text().then(common.mustCall((text) => { + assert.strictEqual(text, ''); + })); + + const h = b.slice(-1, 1); + assert.strictEqual(h.size, 0); + + const i = b.slice(1, 100); + assert.strictEqual(i.size, 9); + + const j = b.slice(1, 2, false); + assert.strictEqual(j.type, 'false'); + + assert.strictEqual(b.size, 10); + assert.strictEqual(b.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, ''); +} + +{ + const descriptor = + Object.getOwnPropertyDescriptor(Blob.prototype, Symbol.toStringTag); + assert.deepStrictEqual(descriptor, { + configurable: true, + enumerable: false, + value: 'Blob', + writable: false + }); +} + +{ + const b = new Blob(['test', 42]); + b.text().then(common.mustCall((text) => { + assert.strictEqual(text, 'test42'); + })); +} + +{ + const b = new Blob(); + assert.strictEqual(inspect(b, { depth: null }), + 'Blob { size: 0, type: \'\' }'); + assert.strictEqual(inspect(b, { depth: -1 }), '[Blob]'); +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 0c3618edca00ba..e38c583511b58e 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -86,6 +86,7 @@ const expectedModules = new Set([ 'NativeModule internal/validators', 'NativeModule internal/vm/module', 'NativeModule internal/worker/js_transferable', + 'NativeModule internal/blob', 'NativeModule path', 'NativeModule timers', 'NativeModule url', diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 957c7f7440a4bc..921fe8fc4bdc86 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -54,6 +54,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..23505105eafa4d --- /dev/null +++ b/test/wpt/status/FileAPI/blob.json @@ -0,0 +1,17 @@ +{ + "Blob-constructor.any.js": { + "skip": "https://github.com/nodejs/node/issues/37358" + }, + "Blob-constructor-dom.window.js": { + "skip": "Depends on DOM API" + }, + "Blob-slice.any.js": { + "skip": "Depends on File API" + }, + "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.mjs b/tools/doc/type-parser.mjs index e49f6898bba3eb..3feac3b211c749 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -38,6 +38,8 @@ const customTypesMap = { 'WebAssembly.Instance': `${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`, + 'Blob': 'buffer.html#buffer_class_blob', + 'Iterable': `${jsDocPrefix}Reference/Iteration_protocols#The_iterable_protocol`, 'Iterator':