From 1e172cd99ad01746b8f95326d90b8f9b45af745c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 15 Oct 2026 19:20:43 +0200 Subject: [PATCH 1/3] buffer: align Blob and File with Web IDL Convert constructor arguments in Web IDL order before processing blob parts, including inherited dictionary members and File.lastModified's long long conversion. Read buffer slots and copy bytes without invoking user getters, slice methods, or species constructors. Treat detached parts as empty and track Blob brands without invoking user getters or accepting proxies. Keep rejecting bare SharedArrayBuffers and shared-memory views for browser compatibility pending resolution of the union conversion rules. Refs: https://github.com/whatwg/webidl/issues/1491 Signed-off-by: Filip Skokan Assisted-by: Codex --- lib/internal/blob.js | 109 ++++++---- lib/internal/file.js | 34 ++-- test/parallel/test-blob-detached-buffer.js | 46 +++++ test/parallel/test-blob-file-webidl.js | 221 +++++++++++++++++++++ 4 files changed, 359 insertions(+), 51 deletions(-) create mode 100644 test/parallel/test-blob-detached-buffer.js create mode 100644 test/parallel/test-blob-file-webidl.js diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 6d359c5adf7e..aea19d6e7471 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -1,7 +1,12 @@ 'use strict'; const { + ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetDetached, ArrayPrototypePush, + DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, MathMax, MathMin, ObjectDefineProperties, @@ -12,10 +17,14 @@ const { PromiseWithResolvers, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, + SafeWeakSet, StringPrototypeSplit, StringPrototypeToLowerCase, Symbol, SymbolToStringTag, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, Uint8Array, } = primordials; @@ -43,7 +52,9 @@ const { const { isAnyArrayBuffer, + isArrayBuffer, isArrayBufferView, + isDataView, } = require('internal/util/types'); const { @@ -55,22 +66,19 @@ const { const { inspect } = require('internal/util/inspect'); const { converters, + createDictionaryConverter, + createEnumConverter, createSequenceConverter, } = require('internal/webidl'); const { codes: { ERR_BUFFER_TOO_LARGE, - ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, ERR_INVALID_THIS, }, } = require('internal/errors'); -const { - validateDictionary, -} = require('internal/validators'); - const { setImmediate, } = require('timers'); @@ -81,6 +89,8 @@ const kHandle = Symbol('kHandle'); const kType = Symbol('kType'); const kLength = Symbol('kLength'); const kNotCloneable = Symbol('kNotCloneable'); +const kAlreadyConverted = Symbol('kAlreadyConverted'); +const blobBrand = new SafeWeakSet(); const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u; @@ -110,16 +120,14 @@ function lazyTextDecoderStream() { const { EOL } = require('internal/constants'); function isBlob(object) { - return object?.[kHandle] !== undefined; + return blobBrand.has(object); } function getSource(source, endings) { if (isBlob(source)) - return [source.size, source[kHandle]]; + return [source[kLength], source[kHandle]]; - if (isAnyArrayBuffer(source)) { - source = new Uint8Array(source); - } else if (!isArrayBufferView(source)) { + if (typeof source === 'string') { if (endings === 'native') source = RegExpPrototypeSymbolReplace(/\r\n|\r|\n/g, source, EOL); source = enc.encode(source); @@ -128,21 +136,49 @@ function getSource(source, endings) { // We copy into a new Uint8Array because the underlying // BackingStores are going to be detached and owned by // the Blob. - const { buffer, byteOffset, byteLength } = source; - const slice = buffer.slice(byteOffset, byteOffset + byteLength); - return [byteLength, new Uint8Array(slice)]; + const isBuffer = isArrayBuffer(source); + const isDataViewSource = !isBuffer && isDataView(source); + const buffer = isBuffer ? source : + isDataViewSource ? DataViewPrototypeGetBuffer(source) : TypedArrayPrototypeGetBuffer(source); + // Check before reading byteOffset and byteLength, whose DataView getters + // throw when the backing buffer is detached. + if (ArrayBufferPrototypeGetDetached(buffer)) + return [0, new Uint8Array()]; + const byteOffset = isBuffer ? 0 : + isDataViewSource ? DataViewPrototypeGetByteOffset(source) : TypedArrayPrototypeGetByteOffset(source); + const byteLength = isBuffer ? ArrayBufferPrototypeGetByteLength(source) : + isDataViewSource ? DataViewPrototypeGetByteLength(source) : TypedArrayPrototypeGetByteLength(source); + const copy = new Uint8Array(new Uint8Array(buffer, byteOffset, byteLength)); + return [byteLength, copy]; } -const sourcesConverter = createSequenceConverter((source, opts = kEmptyObject) => { +const blobPartsConverter = createSequenceConverter((source, opts = kEmptyObject) => { if (isBlob(source)) { return source; } + // Match browsers for bare SharedArrayBuffer pending a resolution of + // https://github.com/whatwg/webidl/issues/1491. if (isAnyArrayBuffer(source) || isArrayBufferView(source)) { return converters.BufferSource(source, opts); } - return converters.DOMString(source, opts); + return converters.USVString(source, opts); }); +const blobPropertyBag = [ + { + key: 'endings', + converter: createEnumConverter('EndingType', ['transparent', 'native']), + defaultValue: () => 'transparent', + }, + { + key: 'type', + converter: converters.DOMString, + defaultValue: () => '', + }, +]; + +const blobPropertyBagConverter = createDictionaryConverter('BlobPropertyBag', blobPropertyBag); + class Blob { /** * @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart @@ -156,40 +192,38 @@ class Blob { * }} [options] * @constructs Blob */ - constructor(sources = [], options) { + constructor(sources = [], options, alreadyConverted) { markTransferMode(this, true, false); - const sources_ = sourcesConverter(sources, { - __proto__: null, - context: 'sources', - }); - - validateDictionary(options, 'options'); - let { - endings = 'transparent', - type = '', - } = options ?? kEmptyObject; - - endings = `${endings}`; - if (endings !== 'transparent' && endings !== 'native') - throw new ERR_INVALID_ARG_VALUE('options.endings', endings); + // File converts all of its arguments before entering the Blob constructor. + if (alreadyConverted !== kAlreadyConverted) { + sources = blobPartsConverter(sources, { + __proto__: null, + context: 'sources', + }); + options = blobPropertyBagConverter(options, { + __proto__: null, + context: 'options', + }); + } + const { endings, type } = options; let length = 0; - for (let i = 0; i < sources_.length; ++i) { - const { 0: len, 1: src } = getSource(sources_[i], endings); + for (let i = 0; i < sources.length; ++i) { + const { 0: len, 1: src } = getSource(sources[i], endings); length += len; - sources_[i] = src; + sources[i] = src; } if (length > kMaxLength) throw new ERR_BUFFER_TOO_LARGE(kMaxLength); - this[kHandle] = _createBlob(sources_, length); + this[kHandle] = _createBlob(sources, length); this[kLength] = length; - type = `${type}`; this[kType] = RegExpPrototypeExec(disallowedTypeCharacters, type) !== null ? '' : StringPrototypeToLowerCase(type); + blobBrand.add(this); } [kInspect](depth, options) { @@ -226,6 +260,7 @@ class Blob { this[kHandle] = handle; this[kType] = type; this[kLength] = length; + blobBrand.add(this); } /** @@ -357,6 +392,7 @@ function TransferableBlob(handle, length, type = '') { this[kHandle] = handle; this[kType] = type; this[kLength] = length; + blobBrand.add(this); } ObjectSetPrototypeOf(TransferableBlob.prototype, Blob.prototype); @@ -701,12 +737,15 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { module.exports = { Blob, + blobPartsConverter, + blobPropertyBag, createBlob, createBlobFromFilePath, createBlobReaderIterable, createBlobReaderStream, getBlobDataSync, isBlob, + kAlreadyConverted, kHandle, kType, resolveObjectURL, diff --git a/lib/internal/file.js b/lib/internal/file.js index aa05a602975e..befbb4e11a30 100644 --- a/lib/internal/file.js +++ b/lib/internal/file.js @@ -3,19 +3,25 @@ const { DateNow, FunctionPrototypeCall, - NumberIsNaN, ObjectDefineProperties, ObjectSetPrototypeOf, - StringPrototypeToWellFormed, Symbol, SymbolToStringTag, } = primordials; const { Blob, + blobPartsConverter, + blobPropertyBag, + kAlreadyConverted, TransferableBlob, } = require('internal/blob'); +const { + converters, + createDictionaryConverter, +} = require('internal/webidl'); + const { customInspectSymbol: kInspect, kEnumerableProperty, @@ -40,6 +46,11 @@ const { const kState = Symbol('state'); +const filePropertyBagConverter = createDictionaryConverter('FilePropertyBag', [ + blobPropertyBag, + [{ key: 'lastModified', converter: converters['long long'] }], +]); + function isFile(object) { return object?.[kState] !== undefined; } @@ -64,22 +75,13 @@ class File extends Blob { throw new ERR_MISSING_ARGS('fileBits', 'fileName'); } - super(fileBits, options); + fileBits = blobPartsConverter(fileBits, { __proto__: null, context: 'fileBits' }); + fileName = converters.USVString(fileName, { __proto__: null, context: 'fileName' }); + options = filePropertyBagConverter(options, { __proto__: null, context: 'options' }); - let { lastModified } = options ?? kEmptyObject; - - if (lastModified !== undefined) { - // Using Number(...) will not throw an error for bigints. - lastModified = +lastModified; - - if (NumberIsNaN(lastModified)) { - lastModified = 0; - } - } else { - lastModified = DateNow(); - } + super(fileBits, options, kAlreadyConverted); - this[kState] = new FileState(StringPrototypeToWellFormed(`${fileName}`), lastModified); + this[kState] = new FileState(fileName, options.lastModified ?? DateNow()); } get name() { diff --git a/test/parallel/test-blob-detached-buffer.js b/test/parallel/test-blob-detached-buffer.js new file mode 100644 index 000000000000..ed3ae2209d3d --- /dev/null +++ b/test/parallel/test-blob-detached-buffer.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { Blob, File } = require('node:buffer'); + +(async () => { + for (const create of [ + (parts, options) => new Blob(parts, options), + (parts, options) => new File(parts, 'test', options), + ]) { + const buffer = new ArrayBuffer(16); + const parts = [ + buffer, + new Uint8Array(buffer), + new Uint8Array(buffer, 4, 8), + new Uint16Array(buffer, 4, 4), + new DataView(buffer), + new DataView(buffer, 4, 8), + Buffer.from(buffer, 4, 8), + ]; + buffer.transfer(); + + for (const part of parts) { + const empty = create([part]); + assert.strictEqual(empty.size, 0); + assert.deepStrictEqual(await empty.arrayBuffer(), new ArrayBuffer(0)); + + const mixed = create(['hello', part, 'world']); + assert.strictEqual(mixed.size, 10); + assert.strictEqual(await mixed.text(), 'helloworld'); + } + + // Options conversion can detach buffers after blob parts conversion. + const anotherBuffer = new ArrayBuffer(16); + const view = new DataView(anotherBuffer, 4, 8); + const blob = create([anotherBuffer, view, 'hello'], { + get endings() { + anotherBuffer.transfer(); + return 'transparent'; + }, + }); + assert.strictEqual(blob.size, 5); + assert.strictEqual(await blob.text(), 'hello'); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-blob-file-webidl.js b/test/parallel/test-blob-file-webidl.js new file mode 100644 index 000000000000..86b05bf7aa66 --- /dev/null +++ b/test/parallel/test-blob-file-webidl.js @@ -0,0 +1,221 @@ +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { Blob, File } = require('node:buffer'); + +(async () => { + for (const create of [ + (parts, options) => new Blob(parts, options), + (parts, options) => new File(parts, 'test', options), + ]) { + // Each dictionary member is converted before the next member is read. + const calls = []; + const blob = create([], { + get endings() { + calls.push('get endings'); + return { + toString() { + calls.push('convert endings'); + return 'transparent'; + }, + }; + }, + get type() { + calls.push('get type'); + return { + toString() { + calls.push('convert type'); + return 'TEXT/PLAIN'; + }, + }; + }, + }); + assert.deepStrictEqual(calls, [ + 'get endings', 'convert endings', 'get type', 'convert type', + ]); + assert.strictEqual(blob.type, 'text/plain'); + + assert.throws(() => create([], { + endings: 'invalid', + get type() { return common.mustNotCall('type must not be read')(); }, + }), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE' }); + + // Dictionary conversion finishes before the input bytes are copied. + const bytes = new Uint8Array([65]); + const changed = create([bytes], { + type: { + toString() { + bytes[0] = 66; + return ''; + }, + }, + }); + assert.strictEqual(await changed.text(), 'B'); + + const detached = new Uint8Array([65]); + const empty = create([detached], { + type: { + toString() { + detached.buffer.transfer(); + return ''; + }, + }, + }); + assert.strictEqual(empty.size, 0); + + // BufferSource copying uses internal slots, not ordinary properties or + // ArrayBuffer.prototype.slice(), which consults Symbol.species. + for (const makePart of [ + (buffer) => buffer, + (buffer) => new Uint8Array(buffer, 1, 2), + (buffer) => new DataView(buffer, 1, 2), + (buffer) => Buffer.from(buffer, 1, 2), + ]) { + const buffer = new Uint8Array([65, 66, 67, 68]).buffer; + const part = makePart(buffer); + if (part !== buffer) { + for (const property of ['buffer', 'byteOffset', 'byteLength']) { + Object.defineProperty(part, property, { + get: common.mustNotCall(`${property} must not be read`), + }); + } + } + buffer.slice = common.mustNotCall('slice must not be called'); + buffer.constructor = { + get [Symbol.species]() { return common.mustNotCall('species must not be read')(); }, + }; + const copied = create([part]); + assert.strictEqual(await copied.text(), part === buffer ? 'ABCD' : 'BC'); + } + + // Reject both bare shared buffers and views for browser compatibility. + for (const part of [ + new SharedArrayBuffer(4), + new Uint8Array(new SharedArrayBuffer(4)), + new DataView(new SharedArrayBuffer(4)), + ]) { + assert.throws(() => create([part]), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + } + for (const part of [ + new ArrayBuffer(4, { maxByteLength: 8 }), + new Uint8Array(new ArrayBuffer(4, { maxByteLength: 8 })), + new DataView(new ArrayBuffer(4, { maxByteLength: 8 })), + ]) { + assert.throws(() => create([part]), TypeError); + } + + const source = new Blob(['abc']); + Object.defineProperty(source, 'size', { + get: common.mustNotCall('size must not be read'), + }); + assert.strictEqual(await create([source]).text(), 'abc'); + + const gets = []; + const proxy = new Proxy(source, { + get(target, key, receiver) { + gets.push(key); + return Reflect.get(target, key, receiver); + }, + }); + assert.strictEqual(await create([proxy]).text(), '[object Blob]'); + assert.deepStrictEqual(gets, [Symbol.toPrimitive, 'toString', Symbol.toStringTag]); + assert.strictEqual(await create([{ __proto__: source }]).text(), '[object Blob]'); + } + + // File arguments are converted from left to right, with the inherited + // BlobPropertyBag members converted before FilePropertyBag.lastModified. + const calls = []; + const file = new File([{ + toString() { + calls.push('part'); + return 'x'; + }, + }], { + toString() { + calls.push('name'); + return 'file\ud800'; + }, + }, { + get endings() { + calls.push('get endings'); + return { + toString() { + calls.push('convert endings'); + return 'transparent'; + }, + }; + }, + get type() { + calls.push('get type'); + return { + toString() { + calls.push('convert type'); + return 'TEXT/PLAIN'; + }, + }; + }, + get lastModified() { + calls.push('get lastModified'); + return { + valueOf() { + calls.push('convert lastModified'); + return 1.9; + }, + }; + }, + }); + assert.deepStrictEqual(calls, [ + 'part', 'name', 'get endings', 'convert endings', 'get type', 'convert type', + 'get lastModified', 'convert lastModified', + ]); + assert.strictEqual(file.name, 'file\ufffd'); + assert.strictEqual(file.type, 'text/plain'); + assert.strictEqual(file.lastModified, 1); + + assert.throws(() => new File(undefined, { + toString: common.mustNotCall('filename must not be converted'), + }), TypeError); + const nameError = new Error('filename conversion'); + assert.throws(() => new File([], { + toString() { throw nameError; }, + }, { + get endings() { return common.mustNotCall('options must not be read')(); }, + }), nameError); + + const bytes = new Uint8Array([65]); + const named = new File([bytes], { + toString() { + bytes[0] = 66; + return 'file'; + }, + }); + assert.strictEqual(await named.text(), 'B'); + + const modified = new File([bytes], 'file', { + lastModified: { + valueOf() { + bytes[0] = 67; + return 1; + }, + }, + }); + assert.strictEqual(await modified.text(), 'C'); + + for (const [lastModified, expected] of [ + [1.9, 1], + [-1.9, -1], + [Infinity, 0], + [-Infinity, 0], + [NaN, 0], + [-0, 0], + [2 ** 63, -(2 ** 63)], + [2 ** 64, 0], + [null, 0], + ]) { + assert.strictEqual(new File([], '', { lastModified }).lastModified, expected); + } +})().then(common.mustCall()); From 766861f9bffc5a7f4c021ea7bfda769496f74c64 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Thu, 15 Oct 2026 16:28:44 +0000 Subject: [PATCH 2/3] test: update WPT for FileAPI to 5878c7a7c0 --- .../wpt/FileAPI/BlobURL/WEB_FEATURES.yml | 2 + .../cross-partition-navigation.https.html | 47 ++++++++++ .../wpt/FileAPI/BlobURL/opaque-origin.html | 47 ++++++++++ .../wpt/FileAPI/FileReader/WEB_FEATURES.yml | 2 + .../wpt/FileAPI/FileReaderSync.worker.js | 8 +- test/fixtures/wpt/FileAPI/WEB_FEATURES.yml | 5 +- .../Blob-array-buffer-data-isolation.html | 85 ++++++++++++++++++ .../Blob-constructor-detached-buffer.any.js | 59 ++++++++++++ .../blob/Blob-constructor-endings.any.js | 35 ++++++++ .../blob/Blob-constructor-endings.html | 35 -------- .../wpt/FileAPI/blob/Blob-constructor.any.js | 19 ++-- .../wpt/FileAPI/blob/Blob-textStream.any.js | 90 +++++++++++++++++++ .../wpt/FileAPI/blob/WEB_FEATURES.yml | 2 + .../file/File-constructor-endings.any.js | 35 ++++++++ .../file/File-constructor-endings.html | 35 -------- .../wpt/FileAPI/file/File-lastModified.html | 18 ++++ .../wpt/FileAPI/file/WEB_FEATURES.yml | 2 + .../FileAPI/filelist-section/WEB_FEATURES.yml | 3 + .../FileReader-multiple-reads.any.js | 63 +++++++++++++ .../reading-data-section/WEB_FEATURES.yml | 2 + .../filereader_abort.any.js | 29 ++++++ .../filereader_readAsDataURL.html | 27 ++++++ ...reader_readAsText_blob_type_charset.any.js | 51 +++++++++++ .../FileAPI/{unicode.html => unicode.any.js} | 27 ++---- .../fixtures/wpt/FileAPI/url/WEB_FEATURES.yml | 2 + .../FileAPI/url/url-in-tags-revoke.window.js | 19 ++++ .../wpt/FileAPI/url/url-in-tags.window.js | 18 ++++ .../url_createobjecturl_file_img-manual.html | 2 +- test/fixtures/wpt/versions.json | 2 +- 29 files changed, 666 insertions(+), 105 deletions(-) create mode 100644 test/fixtures/wpt/FileAPI/BlobURL/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/BlobURL/opaque-origin.html create mode 100644 test/fixtures/wpt/FileAPI/FileReader/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-array-buffer-data-isolation.html create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-constructor-detached-buffer.any.js create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.any.js create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-textStream.any.js create mode 100644 test/fixtures/wpt/FileAPI/blob/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/file/File-constructor-endings.any.js create mode 100644 test/fixtures/wpt/FileAPI/file/File-lastModified.html create mode 100644 test/fixtures/wpt/FileAPI/file/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/filelist-section/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText_blob_type_charset.any.js rename test/fixtures/wpt/FileAPI/{unicode.html => unicode.any.js} (61%) create mode 100644 test/fixtures/wpt/FileAPI/url/WEB_FEATURES.yml diff --git a/test/fixtures/wpt/FileAPI/BlobURL/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/BlobURL/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/BlobURL/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] diff --git a/test/fixtures/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html b/test/fixtures/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html index a92e0f7a8a9b..978370f8805b 100644 --- a/test/fixtures/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html +++ b/test/fixtures/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html @@ -247,5 +247,52 @@ }); }, "Blob URL area element click should enforce noopener for a cross-top-level-site navigation"); +const open_blob_url_window_via_svg_a_click = (blob_url) => ` + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const a = document.createElementNS("http://www.w3.org/2000/svg", "a"); + a.setAttribute("href", "${blob_url}"); + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "opener"); + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.textContent = "click"; + a.appendChild(text); + svg.appendChild(a); + document.body.appendChild(svg); + a.dispatchEvent(new MouseEvent("click", {bubbles: true})); +`; + +// Tests blob URL SVG `` click for same and +// cross partition iframes. +promise_test(t => { + return new Promise(async (resolve, reject) => { + try { + // Creates same and cross partition iframes. + const noopener_response_queue = token(); + + const [cross_site_iframe_uuid, same_site_iframe_uuid] = await create_test_iframes(t, token()); + + const blob = new Blob([opener_check_frame_html(noopener_response_queue)], {type : "text/html"}); + const blob_url = URL.createObjectURL(blob); + + // Attempt to click blob URL in cross partition iframe. + await send(cross_site_iframe_uuid, open_blob_url_window_via_svg_a_click(blob_url)); + const noopener_response_1 = await receive(noopener_response_queue); + if (noopener_response_1 !== opener_null_response) { + reject(`Blob URL page opener wasn't null in not-same-top-level-site iframe.`); + } + + // Attempt to click blob URL in same partition iframe. + await send(same_site_iframe_uuid, open_blob_url_window_via_svg_a_click(blob_url)); + const noopener_response_2 = await receive(noopener_response_queue); + if (noopener_response_2 !== opener_not_null_response) { + reject(`Blob URL page opener was null in same-top-level-site iframe`); + } + resolve(); + } catch (e) { + reject(e); + } + }); +}, "Blob URL SVG element click should enforce noopener for a cross-top-level-site navigation"); + diff --git a/test/fixtures/wpt/FileAPI/BlobURL/opaque-origin.html b/test/fixtures/wpt/FileAPI/BlobURL/opaque-origin.html new file mode 100644 index 000000000000..c17e902128a5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/BlobURL/opaque-origin.html @@ -0,0 +1,47 @@ + + + + +Opaque origin blob URL created from workers + + + + + + + diff --git a/test/fixtures/wpt/FileAPI/FileReader/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/FileReader/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/FileReader/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] diff --git a/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js b/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js index 3d7a0222f312..372cc3c38505 100644 --- a/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js +++ b/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js @@ -28,7 +28,7 @@ test(() => { test(() => { var data = readerSync.readAsDataURL(empty_blob); - assert_equals(data.indexOf("data:"), 0); + assert_equals(data, "data:application/octet-stream;base64,"); }, "readAsDataURL with empty blob"); test(() => { @@ -41,6 +41,12 @@ test(() => { assert_equals(data, ""); }, "readAsBinaryString with empty blob"); +test(() => { + var data = readerSync.readAsBinaryString(new Blob(["σ"])); + assert_equals(data.length, 2, "The result length is 2"); + assert_equals(data, "\xcf\x83", "The result is \xcf\x83"); +}, "readAsBinaryString with multi-byte UTF-8 char"); + test(() => { var data = readerSync.readAsArrayBuffer(blob); assert_true(data instanceof ArrayBuffer); diff --git a/test/fixtures/wpt/FileAPI/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/WEB_FEATURES.yml index 8d279ceaaeab..ea7c8227a285 100644 --- a/test/fixtures/wpt/FileAPI/WEB_FEATURES.yml +++ b/test/fixtures/wpt/FileAPI/WEB_FEATURES.yml @@ -1,3 +1,2 @@ -features: -- name: file - files: "**" +rules: +- "*": [file] diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer-data-isolation.html b/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer-data-isolation.html new file mode 100644 index 000000000000..1bb7f3b2d797 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-array-buffer-data-isolation.html @@ -0,0 +1,85 @@ + + +Blob data isolation from input ArrayBuffers + + + + + + diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor-detached-buffer.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-detached-buffer.any.js new file mode 100644 index 000000000000..1868199cfb83 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-detached-buffer.any.js @@ -0,0 +1,59 @@ +// META: title=Blob constructor with detached ArrayBuffer/ArrayBufferView +// META: script=../support/Blob.js +'use strict'; + +function detachBuffer(buffer) { + // Transfer the buffer to detach it. Works in both window and worker contexts. + new MessageChannel().port1.postMessage(buffer, [buffer]); +} + +test_blob(function() { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view[0] = 0x41; + view[1] = 0x42; + view[2] = 0x43; + view[3] = 0x44; + detachBuffer(buffer); + return new Blob([view]); +}, { + expected: '', + type: '', + desc: 'Blob from a detached ArrayBufferView should be empty' +}); + +test_blob(function() { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer, 1, 2); // offset=1, length=2 + view[0] = 0x41; + view[1] = 0x42; + detachBuffer(buffer); + return new Blob([view]); +}, { + expected: '', + type: '', + desc: 'Blob from a detached ArrayBufferView with offset should be empty' +}); + +test_blob(function() { + const buffer = new ArrayBuffer(4); + detachBuffer(buffer); + return new Blob([buffer]); +}, { + expected: '', + type: '', + desc: 'Blob from a detached ArrayBuffer should be empty' +}); + +test_blob(function() { + const buffer = new ArrayBuffer(4); + const view = new Uint8Array(buffer); + view[0] = 0x41; + detachBuffer(buffer); + // Mix detached view with a string + return new Blob([view, 'hello']); +}, { + expected: 'hello', + type: '', + desc: 'Blob from a detached ArrayBufferView mixed with string, detached part ignored' +}); diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.any.js new file mode 100644 index 000000000000..1057818328e5 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.any.js @@ -0,0 +1,35 @@ +// META: title=Blob constructor: endings option +[ + 'transparent', + 'native' +].forEach(value => test(t => { + assert_class_string(new Blob([], {endings: value}), 'Blob', + `Constructor should allow "${value}" endings`); +}, `Valid "endings" value: ${JSON.stringify(value)}`)); + +[ + null, + '', + 'invalidEnumValue', + 'Transparent', + 'NATIVE', + 0, + {} +].forEach(value => test(t => { + assert_throws_js(TypeError, () => new Blob([], {endings: value}), + 'Blob constructor should throw'); +}, `Invalid "endings" value: ${JSON.stringify(value)}`)); + +test(t => { + const test_error = {name: 'test'}; + assert_throws_exactly( + test_error, + () => new Blob([], { get endings() { throw test_error; }}), + 'Blob constructor should propagate exceptions from "endings" property'); +}, 'Exception propagation from options'); + +test(t => { + let got = false; + new Blob([], { get endings() { got = true; } }); + assert_true(got, 'The "endings" property was accessed during construction.'); +}, 'The "endings" options property is used'); \ 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 index 04edd2a303b1..a395ad78d2ee 100644 --- a/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.html +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor-endings.html @@ -19,41 +19,6 @@ }); } -[ - 'transparent', - 'native' -].forEach(value => test(t => { - assert_class_string(new Blob([], {endings: value}), 'Blob', - `Constructor should allow "${value}" endings`); -}, `Valid "endings" value: ${JSON.stringify(value)}`)); - -[ - null, - '', - 'invalidEnumValue', - 'Transparent', - 'NATIVE', - 0, - {} -].forEach(value => test(t => { - assert_throws_js(TypeError, () => new Blob([], {endings: value}), - 'Blob constructor should throw'); -}, `Invalid "endings" value: ${JSON.stringify(value)}`)); - -test(t => { - const test_error = {name: 'test'}; - assert_throws_exactly( - test_error, - () => new Blob([], { get endings() { throw test_error; }}), - 'Blob constructor should propagate exceptions from "endings" property'); -}, 'Exception propagation from options'); - -test(t => { - let got = false; - new Blob([], { get endings() { got = true; } }); - assert_true(got, 'The "endings" property was accessed during construction.'); -}, 'The "endings" options property is used'); - [ {name: 'LF', input: '\n', native: native_ending}, {name: 'CR', input: '\r', native: native_ending}, diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js index 03155b99cb20..bdcc19416003 100644 --- a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js +++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js @@ -335,6 +335,8 @@ test_blob(function() { desc: "ArrayBuffer elements of the blobParts array should be supported." }); +var isLittleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1; + test_blob(function() { return new Blob([ new Uint8Array([0x50, 0x41, 0x53, 0x53]), @@ -346,7 +348,7 @@ test_blob(function() { new Float32Array([0xD341500000]) ]); }, { - expected: "PASSPASSPASSPASSPASSPASSPASS", + expected: isLittleEndian ? "PASSPASSPASSPASSPASSPASSPASS" : "PASSPASSAPSSAPSSSSAPSSAPSSAP", type: "", desc: "Passing typed arrays as elements of the blobParts array should work." }); @@ -355,20 +357,20 @@ test_blob(function() { new Float16Array([2.65625, 58.59375]) ]); }, { - expected: "PASS", + expected: isLittleEndian ? "PASS" : "APSS", type: "", desc: "Passing a Float16Array as element of the blobParts array should work." }); test_blob(function() { + // 0x535 3415053534150 + // 0x535 = 0b010100110101 -> Sign = +, Exponent = 1333 - 1023 = 310 + // 0x13415053534150 * 2**(-52) + // ==> 0x13415053534150 * 2**258 = 2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680 return new Blob([ - // 0x535 3415053534150 - // 0x535 = 0b010100110101 -> Sign = +, Exponent = 1333 - 1023 = 310 - // 0x13415053534150 * 2**(-52) - // ==> 0x13415053534150 * 2**258 = 2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680 new Float64Array([2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680]) ]); }, { - expected: "PASSPASS", + expected: isLittleEndian ? "PASSPASS" : "SSAPSSAP", type: "", desc: "Passing a Float64Array as element of the blobParts array should work." }); @@ -379,7 +381,7 @@ test_blob(function() { new BigUint64Array([BigInt("0x5353415053534150")]) ]); }, { - expected: "PASSPASSPASSPASS", + expected: isLittleEndian ? "PASSPASSPASSPASS" : "SSAPSSAPSSAPSSAP", type: "", desc: "Passing BigInt typed arrays as elements of the blobParts array should work." }); @@ -528,3 +530,4 @@ type_tests.forEach(function(t) { assert_equals(b.type, t[2]); }, "Blob with type " + format_value(t[1])); }); + diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-textStream.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-textStream.any.js new file mode 100644 index 000000000000..7f22d8bb330d --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/Blob-textStream.any.js @@ -0,0 +1,90 @@ +// META: title=Blob textStream +// META: global=window,worker +// META: script=../support/Blob.js +'use strict'; + +async function readAllChunks(stream) { + const reader = stream.getReader(); + const chunks = []; + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + chunks.push(value); + } + return chunks; +} + +test(() => { + assert_true('textStream' in Blob.prototype, "textStream exists on Blob.prototype"); + assert_equals(typeof Blob.prototype.textStream, "function", "Blob.prototype.textStream is a function"); +}, "textStream method existence"); + +promise_test(async () => { + const blob = new Blob(["hello world"]); + const stream = blob.textStream(); + assert_true(stream instanceof ReadableStream, "textStream() returns a ReadableStream"); + + const chunks = await readAllChunks(stream); + assert_greater_than(chunks.length, 0); + for (const chunk of chunks) { + assert_equals(typeof chunk, "string", "each chunk should be a string"); + } + assert_equals(chunks.join(""), "hello world", "concatenated chunks match the blob content"); +}, "Blob.textStream() basic functionality"); + +promise_test(async () => { + const blob = new Blob(); + const stream = blob.textStream(); + assert_true(stream instanceof ReadableStream); + const chunks = await readAllChunks(stream); + assert_equals(chunks.length, 0, "no chunks should be read from empty blob textStream"); +}, "Blob.textStream() with empty blob"); + +promise_test(async () => { + const blob = new Blob(["hello ", "world"]); + const stream = blob.textStream(); + const chunks = await readAllChunks(stream); + assert_equals(chunks.join(""), "hello world"); +}, "Blob.textStream() with multi-part blob"); + +promise_test(async () => { + const buffer = new Uint8Array([0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00]); // "hello" in UTF-16LE + const blob = new Blob([buffer], { + type: "text/plain; charset=utf-16le" + }); + const stream = blob.textStream(); + const chunks = await readAllChunks(stream); + assert_equals(chunks.join(""), "h\0e\0l\0l\0o\0", "ignores charset=utf-16le type parameter"); +}, "Blob.textStream() ignores type charset (UTF-16LE)"); + +promise_test(async () => { + const blob = new Blob([new TextEncoder().encode("hello")], { + type: "text/plain; charset=invalid-charset" + }); + const stream = blob.textStream(); + const chunks = await readAllChunks(stream); + assert_equals(chunks.join(""), "hello", "ignores invalid-charset type parameter and decodes as UTF-8"); +}, "Blob.textStream() ignores invalid type charset (invalid-charset)"); + +promise_test(async () => { + const blob = new Blob([new TextEncoder().encode("é")], { + type: "text/plain; charset=iso-8859-1" + }); + const stream = blob.textStream(); + const chunks = await readAllChunks(stream); + assert_equals(chunks.join(""), "é", "ignores charset=iso-8859-1 type parameter and decodes as UTF-8"); +}, "Blob.textStream() ignores type charset (iso-8859-1)"); + +promise_test(async () => { + const blob = new Blob(["hello"]); + const stream1 = blob.textStream(); + const stream2 = blob.textStream(); + assert_not_equals(stream1, stream2, "multiple calls return new streams"); + + const chunks1 = await readAllChunks(stream1); + const chunks2 = await readAllChunks(stream2); + assert_equals(chunks1.join(""), "hello"); + assert_equals(chunks2.join(""), "hello"); +}, "Blob.textStream() can be called multiple times"); diff --git a/test/fixtures/wpt/FileAPI/blob/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/blob/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/blob/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] diff --git a/test/fixtures/wpt/FileAPI/file/File-constructor-endings.any.js b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.any.js new file mode 100644 index 000000000000..ac0c0a45a658 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.any.js @@ -0,0 +1,35 @@ +// META: title=File constructor: endings option +[ + 'transparent', + 'native' +].forEach(value => test(t => { + assert_class_string(new File([], "name", {endings: value}), 'File', + `Constructor should allow "${value}" endings`); +}, `Valid "endings" value: ${JSON.stringify(value)}`)); + +[ + null, + '', + 'invalidEnumValue', + 'Transparent', + 'NATIVE', + 0, + {} +].forEach(value => test(t => { + assert_throws_js(TypeError, () => new File([], "name", {endings: value}), + 'File constructor should throw'); +}, `Invalid "endings" value: ${JSON.stringify(value)}`)); + +test(t => { + const test_error = {name: 'test'}; + assert_throws_exactly( + test_error, + () => new File([], "name", { get endings() { throw test_error; }}), + 'File constructor should propagate exceptions from "endings" property'); +}, 'Exception propagation from options'); + +test(t => { + let got = false; + new File([], "name", { get endings() { got = true; } }); + assert_true(got, 'The "endings" property was accessed during construction.'); +}, 'The "endings" options property is used'); \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html index 1282b6c5ac2c..ffbc9867e9b9 100644 --- a/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html +++ b/test/fixtures/wpt/FileAPI/file/File-constructor-endings.html @@ -19,41 +19,6 @@ }); } -[ - 'transparent', - 'native' -].forEach(value => test(t => { - assert_class_string(new File([], "name", {endings: value}), 'File', - `Constructor should allow "${value}" endings`); -}, `Valid "endings" value: ${JSON.stringify(value)}`)); - -[ - null, - '', - 'invalidEnumValue', - 'Transparent', - 'NATIVE', - 0, - {} -].forEach(value => test(t => { - assert_throws_js(TypeError, () => new File([], "name", {endings: value}), - 'File constructor should throw'); -}, `Invalid "endings" value: ${JSON.stringify(value)}`)); - -test(t => { - const test_error = {name: 'test'}; - assert_throws_exactly( - test_error, - () => new File([], "name", { get endings() { throw test_error; }}), - 'File constructor should propagate exceptions from "endings" property'); -}, 'Exception propagation from options'); - -test(t => { - let got = false; - new File([], "name", { get endings() { got = true; } }); - assert_true(got, 'The "endings" property was accessed during construction.'); -}, 'The "endings" options property is used'); - [ {name: 'LF', input: '\n', native: native_ending}, {name: 'CR', input: '\r', native: native_ending}, diff --git a/test/fixtures/wpt/FileAPI/file/File-lastModified.html b/test/fixtures/wpt/FileAPI/file/File-lastModified.html new file mode 100644 index 000000000000..b61e1b9b4d9f --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/File-lastModified.html @@ -0,0 +1,18 @@ + + +File's lastModified + + + + + diff --git a/test/fixtures/wpt/FileAPI/file/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/file/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/file/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] diff --git a/test/fixtures/wpt/FileAPI/filelist-section/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/filelist-section/WEB_FEATURES.yml new file mode 100644 index 000000000000..841cbf85c271 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/filelist-section/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +rules: +- filelist_multiple_selected_files-manual.html: [input-file-multiple] +- "*": [file] diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js index 4b19c69b4251..25ff25b06d66 100644 --- a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js +++ b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js @@ -79,3 +79,66 @@ async_test(function() { }); reader.readAsText(blob_1); }, 'test abort and restart in onloadstart event for readAsText'); + +promise_test(async t => { + const reader = new FileReader(); + const events = []; + + for (const type of ['abort', 'load', 'loadend']) { + reader.addEventListener(type, () => events.push(type)); + } + + reader.addEventListener('error', t.unreached_func('Unexpected error event')); + reader.addEventListener('abort', t.step_func(() => { + reader.readAsText(new Blob(['second'])); + }), + {once: true}); + + const firstLoadstart = new Promise(resolve => { + reader.addEventListener('loadstart', resolve, {once: true}); + }); + const secondLoadend = new Promise(resolve => { + reader.addEventListener('loadend', t.step_func(() => { + if (reader.result === 'second') { + resolve(); + } + })); + }); + + reader.readAsText(new Blob([new Uint8Array(1024 * 1024)])); + await firstLoadstart; + reader.abort(); + await secondLoadend; + + assert_array_equals(events, ['abort', 'load', 'loadend']); +}, 'A new read in an abort handler suppresses the old read loadend'); + +promise_test(async t => { + const reader = new FileReader(); + const events = []; + let load_count = 0; + + for (const type of ['load', 'loadend']) { + reader.addEventListener(type, () => events.push(type)); + } + + reader.addEventListener('error', t.unreached_func('Unexpected error event')); + reader.addEventListener('load', t.step_func(() => { + if (++load_count === 1) { + reader.readAsText(new Blob(['second'])); + } + })); + + const secondLoadend = new Promise(resolve => { + reader.addEventListener('loadend', t.step_func(() => { + if (reader.result === 'second') { + resolve(); + } + })); + }); + + reader.readAsText(new Blob(['first'])); + await secondLoadend; + + assert_array_equals(events, ['load', 'load', 'loadend']); +}, 'A new read in a load handler suppresses the old read loadend'); diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/reading-data-section/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js index c778ae55bb57..2563dce5ec2d 100644 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js @@ -36,3 +36,32 @@ assert_equals(readerAbort.readyState, readerAbort.DONE); }); }, "Aborting after read"); + + promise_test(t => { + var reader = new FileReader(); + + var eventWatcher = new EventWatcher(t, reader, + ['abort', 'loadstart', 'loadend', 'error', 'load']); + + // First read: run to completion so the reader reaches the DONE state. + reader.readAsText(new Blob(["first read"])); + return eventWatcher.wait_for('loadstart') + .then(() => eventWatcher.wait_for('load')) + .then(() => eventWatcher.wait_for('loadend')) + .then(() => { + assert_equals(reader.readyState, reader.DONE); + + // Reuse the same reader for a second read and abort it while it is + // still LOADING. 'abort' and 'loadend' are dispatched synchronously, + // so call wait_for before calling abort. + reader.readAsText(new Blob(["second read"])); + assert_equals(reader.readyState, reader.LOADING); + var nextEvent = eventWatcher.wait_for(['abort', 'loadend']); + reader.abort(); + return nextEvent; + }) + .then(() => { + assert_equals(reader.result, null); + assert_equals(reader.readyState, reader.DONE); + }); + }, "Aborting a reused reader after a previous read completed"); 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 000000000000..70ca365616d8 --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html @@ -0,0 +1,27 @@ + + +FileAPI Test: FileReader.readAsDataURL + + + + + diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText_blob_type_charset.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText_blob_type_charset.any.js new file mode 100644 index 000000000000..3692fbc2928b --- /dev/null +++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText_blob_type_charset.any.js @@ -0,0 +1,51 @@ +// META: title=FileAPI Test: readAsText uses the Blob type's charset when no encoding argument is given + +// Per the File API "read as text" algorithm, the encoding is determined in this +// order: (1) the explicit `encoding` argument if it is a supported encoding; +// (2) otherwise, if the blob's `type` has a `charset` parameter, that charset; +// (3) otherwise UTF-8. These tests exercise step (2) with BOM-free data so that +// the charset parameter is the only thing that can select the right decoder +// (i.e. they are not accidentally satisfied by BOM sniffing). + +async_test(function() { + // 0x80 is the Euro sign (U+20AC) in windows-1252. As UTF-8 it is a lone + // continuation byte and decodes to U+FFFD. + var blob = new Blob([new Uint8Array([0x80])], { type: "text/plain;charset=windows-1252" }); + var reader = new FileReader(); + + reader.onloadend = this.step_func_done(function() { + assert_equals(reader.result, "€", + "readAsText should honor the windows-1252 charset from the Blob type and decode 0x80 as U+20AC, not UTF-8 U+FFFD."); + }); + + reader.readAsText(blob); +}, "readAsText() uses the charset parameter of the Blob's type (windows-1252)"); + +async_test(function() { + // "héllo" in windows-1252: 0xE9 is 'é'. As UTF-8, the lone 0xE9 is an + // invalid lead byte and decodes to U+FFFD. + var blob = new Blob([new Uint8Array([0x68, 0xE9, 0x6C, 0x6C, 0x6F])], { type: "text/plain;charset=windows-1252" }); + var reader = new FileReader(); + + reader.onloadend = this.step_func_done(function() { + assert_equals(reader.result, "héllo", + "readAsText should decode the body using the windows-1252 charset declared in the Blob type."); + }); + + reader.readAsText(blob); +}, "readAsText() decodes a windows-1252 Blob body using the type's charset"); + +async_test(function() { + // An explicit encoding argument must still win over the Blob type charset + // (step 1 of the algorithm). This already works in WebKit and guards against + // a fix that over-corrects by always preferring the type charset. + var blob = new Blob([new Uint8Array([0x80])], { type: "text/plain;charset=UTF-8" }); + var reader = new FileReader(); + + reader.onloadend = this.step_func_done(function() { + assert_equals(reader.result, "€", + "An explicit encoding argument takes precedence over the Blob type's charset."); + }); + + reader.readAsText(blob, "windows-1252"); +}, "readAsText() encoding argument overrides the Blob type's charset"); diff --git a/test/fixtures/wpt/FileAPI/unicode.html b/test/fixtures/wpt/FileAPI/unicode.any.js similarity index 61% rename from test/fixtures/wpt/FileAPI/unicode.html rename to test/fixtures/wpt/FileAPI/unicode.any.js index ce3e3579d7c2..841b937a67f6 100644 --- a/test/fixtures/wpt/FileAPI/unicode.html +++ b/test/fixtures/wpt/FileAPI/unicode.any.js @@ -1,46 +1,31 @@ - - -Blob/Unicode interaction: normalization and encoding - - - diff --git a/test/fixtures/wpt/FileAPI/url/WEB_FEATURES.yml b/test/fixtures/wpt/FileAPI/url/WEB_FEATURES.yml new file mode 100644 index 000000000000..69d4514d8f3a --- /dev/null +++ b/test/fixtures/wpt/FileAPI/url/WEB_FEATURES.yml @@ -0,0 +1,2 @@ +rules: +- "**": [file] 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 index 1cdad79f7e34..476daba6f1b3 100644 --- a/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js +++ b/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js @@ -94,11 +94,30 @@ async_test(t => { e.onload = t.step_func_done(() => { assert_equals(window.script_test_result, run_result); }); + e.onerror = t.unreached_func(); document.body.appendChild(e); URL.revokeObjectURL(url); }, 'Fetching a blob URL immediately before revoking it works in