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