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 @@
+
+
+