From 565cf51f5187911533a6f038547465de70aaf932 Mon Sep 17 00:00:00 2001 From: Manuel Jasso Date: Mon, 5 Aug 2024 12:12:59 -0700 Subject: [PATCH] @W-16383704: Convert near-membrane's binary data tests to MaxPerf/MaxCompat (#459) * Upgrade Karma Firefox Launcher * Integrate file-fixtures-preprocessor This is to be able to have real js files for testing in the sandbox * Renamed `remapTypedArrays` to `maxCompatMode` * Centralized all the objects that must be from the same global object even when maxCompatMode is false * Created `maxPerfModeKeys` array and added `Atomics` to it * Renamed maxCompatMode to maxPerfMode and flipped the logic Split the maxPerfModeKeys into intrinsics and browser, ok for now, to be improved * Added .npmrc and .yarnrc Also removed Husky's commit-msg --- .eslintignore | 1 + .husky/commit-msg | 4 - .npmrc | 4 + .yarnrc | 1 + karma.config.js | 4 +- package.json | 6 +- .../src/__tests__/intrinsics.spec.ts | 2 +- packages/near-membrane-base/src/intrinsics.ts | 37 +- .../near-membrane-dom/src/browser-realm.ts | 13 +- packages/near-membrane-dom/src/types.ts | 2 +- packages/near-membrane-dom/src/window.ts | 27 +- packages/near-membrane-node/src/node-realm.ts | 8 +- packages/near-membrane-node/src/types.ts | 2 +- test/membrane/binary-data.spec.js | 809 ++---------------- .../untrusted/binary-data/FileSaver-blob.js | 9 + .../untrusted/binary-data/FileSaver-file.js | 11 + .../untrusted/binary-data/FileSaver.js | 172 ++++ .../untrusted/binary-data/always-remaps.js | 2 + .../membrane/untrusted/binary-data/atomics.js | 11 + test/membrane/untrusted/binary-data/blob.js | 6 + test/membrane/untrusted/binary-data/crypto.js | 23 + .../untrusted/binary-data/data-view.js | 3 + .../untrusted/binary-data/file-reader.js | 10 + .../binary-data/typed-array-modified.js | 18 + .../binary-data/typed-array-out-of-bound.js | 18 + .../binary-data/typed-array-subarray.js | 17 + .../binary-data/typed-array-subclass.js | 24 + .../untrusted/binary-data/typed-array.js | 19 + .../untrusted/binary-data/url-file-blob.js | 7 + .../untrusted/binary-data/url-html.js | 4 + .../membrane/untrusted/binary-data/url-svg.js | 9 + .../untrusted/binary-data/url-typed-array.js | 5 + .../membrane/untrusted/binary-data/url-xml.js | 4 + yarn.lock | 46 +- 34 files changed, 540 insertions(+), 798 deletions(-) delete mode 100755 .husky/commit-msg create mode 100644 .npmrc create mode 100644 .yarnrc create mode 100644 test/membrane/untrusted/binary-data/FileSaver-blob.js create mode 100644 test/membrane/untrusted/binary-data/FileSaver-file.js create mode 100644 test/membrane/untrusted/binary-data/FileSaver.js create mode 100644 test/membrane/untrusted/binary-data/always-remaps.js create mode 100644 test/membrane/untrusted/binary-data/atomics.js create mode 100644 test/membrane/untrusted/binary-data/blob.js create mode 100644 test/membrane/untrusted/binary-data/crypto.js create mode 100644 test/membrane/untrusted/binary-data/data-view.js create mode 100644 test/membrane/untrusted/binary-data/file-reader.js create mode 100644 test/membrane/untrusted/binary-data/typed-array-modified.js create mode 100644 test/membrane/untrusted/binary-data/typed-array-out-of-bound.js create mode 100644 test/membrane/untrusted/binary-data/typed-array-subarray.js create mode 100644 test/membrane/untrusted/binary-data/typed-array-subclass.js create mode 100644 test/membrane/untrusted/binary-data/typed-array.js create mode 100644 test/membrane/untrusted/binary-data/url-file-blob.js create mode 100644 test/membrane/untrusted/binary-data/url-html.js create mode 100644 test/membrane/untrusted/binary-data/url-svg.js create mode 100644 test/membrane/untrusted/binary-data/url-typed-array.js create mode 100644 test/membrane/untrusted/binary-data/url-xml.js diff --git a/.eslintignore b/.eslintignore index b750a1fe..7d2ecca0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ dist/ node_modules/ types/ */**/bundle.js +**/untrusted/**/*.js diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 44c73fd9..00000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -yarn commitlint --edit "$1" diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..93ee507a --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +registry=https://registry.yarnpkg.com +save-exact=true +package-lock=false +scripts-prepend-node-path=true diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..fdd705c6 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +save-prefix "" diff --git a/karma.config.js b/karma.config.js index aede4434..7503c2b9 100644 --- a/karma.config.js +++ b/karma.config.js @@ -2,10 +2,10 @@ 'use strict'; +const path = require('node:path'); const globby = require('globby'); const istanbul = require('rollup-plugin-istanbul'); const { nodeResolve } = require('@rollup/plugin-node-resolve'); -const path = require('node:path'); process.env.CHROME_BIN = require('puppeteer').executablePath(); @@ -38,6 +38,7 @@ const customLaunchers = { module.exports = function (config) { const bootstrapFilesPattern = 'test/__bootstrap__/**/*.js'; + const fileFixturesPattern = '**/untrusted/**/*.js'; const karmaConfig = { basePath, browsers: Object.keys(customLaunchers), @@ -58,6 +59,7 @@ module.exports = function (config) { logLevel: config.LOG_ERROR, preprocessors: { [bootstrapFilesPattern]: ['rollup'], + [fileFixturesPattern]: ['file-fixtures'], [testFilesPattern]: ['rollup'], }, reporters: ['progress'], diff --git a/package.json b/package.json index e57433ef..e2b541ad 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "karma": "6.4.2", "karma-chrome-launcher": "3.2.0", "karma-coverage": "2.2.1", - "karma-firefox-launcher": "2.1.2", + "karma-file-fixtures-preprocessor": "^3.0.2", + "karma-firefox-launcher": "2.1.3", "karma-jasmine": "5.1.0", "karma-rollup-preprocessor": "7.0.7", "karma-safari-launcher": "1.0.0", @@ -102,8 +103,7 @@ }, "husky": { "hooks": { - "pre-commit": "lint-staged", - "commit-msg": "yarn commitlint --edit $1" + "pre-commit": "lint-staged" } }, "dependencies": {} diff --git a/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts b/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts index 53f1777a..dd1ee020 100644 --- a/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts +++ b/packages/near-membrane-base/src/__tests__/intrinsics.spec.ts @@ -88,7 +88,6 @@ const ReflectiveIntrinsicObjectNames = [ ]; const RemappedIntrinsicObjectNames = [ - 'Atomics', 'Date', 'Intl', 'Map', @@ -100,6 +99,7 @@ const RemappedIntrinsicObjectNames = [ const TypedAraysInstrinsics = [ 'ArrayBuffer', + 'Atomics', 'BigInt64Array', 'BigUint64Array', 'DataView', diff --git a/packages/near-membrane-base/src/intrinsics.ts b/packages/near-membrane-base/src/intrinsics.ts index 79648916..afca40ea 100644 --- a/packages/near-membrane-base/src/intrinsics.ts +++ b/packages/near-membrane-base/src/intrinsics.ts @@ -27,7 +27,7 @@ import { VirtualEnvironment } from './environment'; * problematic, and requires a lot more work to guarantee that objects from both sides * can be considered equivalents (without identity discontinuity). */ -function getESGlobalKeys(remapTypedArrays = true) { +function getESGlobalKeys(maxPerfMode: boolean) { const ESGlobalKeys = [ // *** 19.1 Value Properties of the Global Object 'globalThis', @@ -91,23 +91,31 @@ function getESGlobalKeys(remapTypedArrays = true) { // 'Intl', // Remapped ]; - if (remapTypedArrays === false) { - ESGlobalKeys.push( + // This set is for maxPerfMode, all of these must be from the same global object + const maxPerfModeKeys = { + intrinsics: [ 'ArrayBuffer', + 'Atomics', 'BigInt64Array', 'BigUint64Array', 'DataView', 'Float32Array', 'Float64Array', - 'Int8Array', 'Int16Array', 'Int32Array', + 'Int8Array', 'SharedArrayBuffer', + 'Uint16Array', + 'Uint32Array', 'Uint8Array', 'Uint8ClampedArray', - 'Uint16Array', - 'Uint32Array' - ); + ], + // Ideally these should come from browser-realm, that's a code reorg improvement for later + browser: ['Blob', 'crypto', 'Crypto', 'File', 'FileReader', 'SubtleCrypto', 'URL'], + }; + + if (maxPerfMode) { + ESGlobalKeys.push(...maxPerfModeKeys.intrinsics, ...maxPerfModeKeys.browser); } return ESGlobalKeys; } @@ -131,8 +139,8 @@ const ReflectiveIntrinsicObjectNames = [ 'globalThis', ]; -function getESGlobalsAndReflectiveIntrinsicObjectNames(remapTypedArrays = true) { - const ESGlobalKeys = getESGlobalKeys(remapTypedArrays); +function getESGlobalsAndReflectiveIntrinsicObjectNames(maxPerfMode: boolean) { + const ESGlobalKeys = getESGlobalKeys(maxPerfMode); return toSafeArray([...ESGlobalKeys, ...ReflectiveIntrinsicObjectNames]); } @@ -149,10 +157,10 @@ function getGlobalObjectOwnKeys(source: object): PropertyKey[] { export function assignFilteredGlobalDescriptorsFromPropertyDescriptorMap< T extends PropertyDescriptorMap ->(descs: T, source: PropertyDescriptorMap, includeTypedArrays?: boolean): T { +>(descs: T, source: PropertyDescriptorMap, maxPerfMode: boolean): T { const ownKeys = getGlobalObjectOwnKeys(source); const ESGlobalsAndReflectiveIntrinsicObjectNames = - getESGlobalsAndReflectiveIntrinsicObjectNames(includeTypedArrays); + getESGlobalsAndReflectiveIntrinsicObjectNames(maxPerfMode); for (let i = 0, { length } = ownKeys; i < length; i += 1) { const ownKey = ownKeys[i]; // Avoid overriding ECMAScript global names that correspond to @@ -172,15 +180,12 @@ export function assignFilteredGlobalDescriptorsFromPropertyDescriptorMap< return descs; } -export function getFilteredGlobalOwnKeys( - source: object, - includeTypedArrays?: boolean -): PropertyKey[] { +export function getFilteredGlobalOwnKeys(source: object, maxPerfMode: boolean): PropertyKey[] { const result: PropertyKey[] = []; let resultOffset = 0; const ownKeys = getGlobalObjectOwnKeys(source); const ESGlobalsAndReflectiveIntrinsicObjectNames = - getESGlobalsAndReflectiveIntrinsicObjectNames(includeTypedArrays); + getESGlobalsAndReflectiveIntrinsicObjectNames(maxPerfMode); for (let i = 0, { length } = ownKeys; i < length; i += 1) { const ownKey = ownKeys[i]; // Avoid overriding ECMAScript global names that correspond to global diff --git a/packages/near-membrane-dom/src/browser-realm.ts b/packages/near-membrane-dom/src/browser-realm.ts index 7879a3df..4332f401 100644 --- a/packages/near-membrane-dom/src/browser-realm.ts +++ b/packages/near-membrane-dom/src/browser-realm.ts @@ -78,7 +78,7 @@ function createIframeVirtualEnvironment( instrumentation, keepAlive = true, liveTargetCallback, - remapTypedArrays = true, + maxPerfMode = false, signSourceCallback, // eslint-disable-next-line prefer-object-spread } = ObjectAssign({ __proto__: null }, providedOptions) as BrowserEnvironmentOptions; @@ -90,10 +90,7 @@ function createIframeVirtualEnvironment( )!; const shouldUseDefaultGlobalOwnKeys = typeof globalObjectShape !== 'object' || globalObjectShape === null; - const defaultGlobalOwnKeys = filterWindowKeys( - getFilteredGlobalOwnKeys(redWindow, remapTypedArrays), - remapTypedArrays - ); + const defaultGlobalOwnKeys = filterWindowKeys(getFilteredGlobalOwnKeys(redWindow, maxPerfMode)); let blueConnector = blueCreateHooksCallbackCache.get(blueRefs.document) as | Connector | undefined; @@ -142,7 +139,7 @@ function createIframeVirtualEnvironment( blueRefs.window, shouldUseDefaultGlobalOwnKeys ? (defaultGlobalOwnKeys as PropertyKey[]) - : filterWindowKeys(getFilteredGlobalOwnKeys(globalObjectShape), remapTypedArrays), + : filterWindowKeys(getFilteredGlobalOwnKeys(globalObjectShape, maxPerfMode)), // Chromium based browsers have a bug that nulls the result of `window` // getters in detached iframes when the property descriptor of `window.window` // is retrieved. @@ -154,9 +151,9 @@ function createIframeVirtualEnvironment( assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( filteredEndowments, endowments, - remapTypedArrays + maxPerfMode ); - removeWindowDescriptors(filteredEndowments, remapTypedArrays); + removeWindowDescriptors(filteredEndowments); env.remapProperties(blueRefs.window, filteredEndowments); } // We intentionally skip remapping Window.prototype because there is nothing diff --git a/packages/near-membrane-dom/src/types.ts b/packages/near-membrane-dom/src/types.ts index 0b655e6f..63f01ac5 100644 --- a/packages/near-membrane-dom/src/types.ts +++ b/packages/near-membrane-dom/src/types.ts @@ -13,6 +13,6 @@ export interface BrowserEnvironmentOptions { instrumentation?: Instrumentation; keepAlive?: boolean; liveTargetCallback?: LiveTargetCallback; - remapTypedArrays?: boolean; + maxPerfMode?: boolean; signSourceCallback?: SignSourceCallback; } diff --git a/packages/near-membrane-dom/src/window.ts b/packages/near-membrane-dom/src/window.ts index 15c972d8..b5195287 100644 --- a/packages/near-membrane-dom/src/window.ts +++ b/packages/near-membrane-dom/src/window.ts @@ -65,18 +65,8 @@ export function getCachedGlobalObjectReferences( return record; } -export function filterWindowKeys(keys: PropertyKey[], remapTypedArrays: boolean): PropertyKey[] { +export function filterWindowKeys(keys: PropertyKey[]): PropertyKey[] { const excludedKeys = new SetCtor(['document', 'location', 'top', 'window']); - // Crypto and typed arrays must be from the same global object - if (remapTypedArrays === false) { - excludedKeys.add('crypto'); - excludedKeys.add('Crypto'); - excludedKeys.add('SubtleCrypto'); - excludedKeys.add('Blob'); - excludedKeys.add('File'); - excludedKeys.add('FileReader'); - excludedKeys.add('URL'); - } const result: PropertyKey[] = []; let resultOffset = 0; for (let i = 0, { length } = keys; i < length; i += 1) { @@ -116,10 +106,7 @@ export function filterWindowKeys(keys: PropertyKey[], remapTypedArrays: boolean) * that will be installed (via the membrane) as global descriptors in * the red realm. */ -export function removeWindowDescriptors( - unsafeDescs: T, - remapTypedArrays: boolean -): T { +export function removeWindowDescriptors(unsafeDescs: T): T { // Remove unforgeable descriptors that cannot be installed. ReflectDeleteProperty(unsafeDescs, 'document'); ReflectDeleteProperty(unsafeDescs, 'location'); @@ -127,16 +114,6 @@ export function removeWindowDescriptors( ReflectDeleteProperty(unsafeDescs, 'window'); // Remove other browser specific unforgeables. ReflectDeleteProperty(unsafeDescs, 'chrome'); - // Crypto and typed arrays must be from the same global object - if (remapTypedArrays === false) { - ReflectDeleteProperty(unsafeDescs, 'crypto'); - ReflectDeleteProperty(unsafeDescs, 'Crypto'); - ReflectDeleteProperty(unsafeDescs, 'SubtleCrypto'); - ReflectDeleteProperty(unsafeDescs, 'Blob'); - ReflectDeleteProperty(unsafeDescs, 'File'); - ReflectDeleteProperty(unsafeDescs, 'FileReader'); - ReflectDeleteProperty(unsafeDescs, 'URL'); - } return unsafeDescs; } diff --git a/packages/near-membrane-node/src/node-realm.ts b/packages/near-membrane-node/src/node-realm.ts index 885c4503..f69981c9 100644 --- a/packages/near-membrane-node/src/node-realm.ts +++ b/packages/near-membrane-node/src/node-realm.ts @@ -33,7 +33,7 @@ export default function createVirtualEnvironment( globalObjectShape, instrumentation, liveTargetCallback, - remapTypedArrays, + maxPerfMode = false, signSourceCallback, } = ObjectAssign({ __proto__: null }, providedOptions) as NodeEnvironmentOptions; let blueConnector = blueCreateHooksCallbackCache.get(globalObject) as Connector | undefined; @@ -60,14 +60,14 @@ export default function createVirtualEnvironment( const shouldUseDefaultGlobalOwnKeys = typeof globalObjectShape !== 'object' || globalObjectShape === null; if (shouldUseDefaultGlobalOwnKeys && defaultGlobalOwnKeys === null) { - defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(redGlobalObject, remapTypedArrays); + defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(redGlobalObject, maxPerfMode); } env.lazyRemapProperties( globalObject, shouldUseDefaultGlobalOwnKeys ? (defaultGlobalOwnKeys as PropertyKey[]) - : getFilteredGlobalOwnKeys(globalObjectShape) + : getFilteredGlobalOwnKeys(globalObjectShape, maxPerfMode) ); if (endowments) { @@ -75,7 +75,7 @@ export default function createVirtualEnvironment( assignFilteredGlobalDescriptorsFromPropertyDescriptorMap( filteredEndowments, endowments, - remapTypedArrays + maxPerfMode ); env.remapProperties(globalObject, filteredEndowments); } diff --git a/packages/near-membrane-node/src/types.ts b/packages/near-membrane-node/src/types.ts index 36420557..a879ad4e 100644 --- a/packages/near-membrane-node/src/types.ts +++ b/packages/near-membrane-node/src/types.ts @@ -11,6 +11,6 @@ export interface NodeEnvironmentOptions { globalObjectShape?: object; instrumentation?: Instrumentation; liveTargetCallback?: LiveTargetCallback; - remapTypedArrays?: boolean; + maxPerfMode?: boolean; signSourceCallback?: SignSourceCallback; } diff --git a/test/membrane/binary-data.spec.js b/test/membrane/binary-data.spec.js index d2dae597..dcaaadd5 100644 --- a/test/membrane/binary-data.spec.js +++ b/test/membrane/binary-data.spec.js @@ -1,14 +1,15 @@ +/* eslint-disable no-underscore-dangle, no-alert */ + import createVirtualEnvironment from '@locker/near-membrane-dom'; +const untrusted = (name) => window.__FIXTURES__[`test/membrane/untrusted/binary-data/${name}`]; + function createEnvironmentThatAlwaysRemapsTypedArray() { const alwayRemapping = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect, OuterUint8Array: Uint8Array }), }); - alwayRemapping.evaluate(` - const u8a = new Uint8Array(); - expect(u8a instanceof OuterUint8Array).toBe(true); - `); + alwayRemapping.evaluate(untrusted('always-remaps.js')); } // Safari Technology Preview may not have support for Atomics enabled. if (typeof Atomics !== 'undefined') { @@ -20,42 +21,16 @@ if (typeof Atomics !== 'undefined') { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT); - const i32a = new Int32Array(ab); - i32a[0] = 9; - Atomics.add(i32a, 0, 33); // 42 - Atomics.and(i32a, 0, 1); // 0 - Atomics.exchange(i32a, 0, 42); // 42 - Atomics.or(i32a, 0, 1); // 43 - Atomics.store(i32a, 0, 18); // 18 - Atomics.sub(i32a, 0, 10); - Atomics.xor(i32a, 0, 1); - expect(Atomics.load(i32a, 0)).toBe(9); - `); - }); - - it('fails to operate on atomic-friendly typed arrays, when typed arrays are not remapped', () => { + env.evaluate(untrusted('atomics.js')); + }); + + it('operates on atomic-friendly typed arrays, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - expect(() => - env.evaluate(` - const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT); - const i32a = new Int32Array(ab); - i32a[0] = 9; - Atomics.add(i32a, 0, 33); // 42 - Atomics.and(i32a, 0, 1); // 0 - Atomics.exchange(i32a, 0, 42); // 42 - Atomics.or(i32a, 0, 1); // 43 - Atomics.store(i32a, 0, 18); // 18 - Atomics.sub(i32a, 0, 10); - Atomics.xor(i32a, 0, 1); - expect(Atomics.load(i32a, 0)).toBe(9); - `) - ).toThrow(); + env.evaluate(untrusted('atomics.js')); }); }); } @@ -68,24 +43,15 @@ describe('Blob', () => { endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); - env.evaluate(` - const a = new Uint8Array([97, 98, 99]); - const b = new Blob([a], { type: 'application/octet-stream' }); - b.text().then((output) => { - expect(output).toBe('abc'); - done(); - }); - `); + env.evaluate(untrusted('blob.js')); }); - it('fails to encode blobs from typed arrays, when typed arrays are not remapped', () => { + it('encode blobs from typed arrays, when maxPerfMode is true', (done) => { const env = createVirtualEnvironment(window, { - remapTypedArrays: false, + endowments: Object.getOwnPropertyDescriptors({ done, expect }), + maxPerfMode: true, }); - env.evaluate(` - const a = new Uint8Array([97, 98, 99]); - const b = new Blob([a], { type: 'application/octet-stream' }); - `); + env.evaluate(untrusted('blob.js')); }); }); @@ -97,119 +63,23 @@ describe('Crypto', () => { endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); - env.evaluate(` - expect(() => { - crypto.getRandomValues(new Uint8Array(1)); - }).not.toThrow(); - async function f() { - const algorithm = { name: "RSA-OAEP" }; - const data = new Uint8Array([255]); - const keyPair = await window.crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["encrypt", "decrypt"], - ); - - const encrypted = await crypto.subtle.encrypt( - algorithm, - keyPair.publicKey, - data - ); - - const decrypted = await crypto.subtle.decrypt( - algorithm, - keyPair.privateKey, - encrypted - ); - } - - f().then(done); - `); + env.evaluate(untrusted('crypto.js')); }); - it('creates random values from typed arrays, when typed arrays are not remapped', (done) => { + it('creates random values from typed arrays, when maxPerfMode is true', (done) => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ done, expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - expect(() => { - crypto.getRandomValues(new Uint8Array(1)); - }).not.toThrow(); - async function f() { - const algorithm = { name: "RSA-OAEP" }; - const data = new Uint8Array([255]); - const keyPair = await window.crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["encrypt", "decrypt"], - ); - - const encrypted = await crypto.subtle.encrypt( - algorithm, - keyPair.publicKey, - data - ); - - const decrypted = await crypto.subtle.decrypt( - algorithm, - keyPair.privateKey, - encrypted - ); - } - - f().then(done); - `); + env.evaluate(untrusted('crypto.js')); }); - it('ignores the presense of crypto in endowments if remapTypedArrays is false', (done) => { + it('ignores the presence of crypto in endowments if maxPerfMode is true', (done) => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ Crypto, crypto, done, expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - expect(() => { - crypto.getRandomValues(new Uint8Array(1)); - }).not.toThrow(); - async function f() { - const algorithm = { name: "RSA-OAEP" }; - const data = new Uint8Array([255]); - const keyPair = await window.crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["encrypt", "decrypt"], - ); - - const encrypted = await crypto.subtle.encrypt( - algorithm, - keyPair.publicKey, - data - ); - - const decrypted = await crypto.subtle.decrypt( - algorithm, - keyPair.privateKey, - encrypted - ); - } - - f().then(done); - `); + env.evaluate(untrusted('crypto.js')); }); }); @@ -221,23 +91,15 @@ describe('DataView', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const dataView = new DataView(buffer); - expect(dataView[0]).toBe(undefined); - `); + env.evaluate(untrusted('data-view.js')); }); - it('should not support index access, when typed arrays are not remapped', () => { + it('should not support index access, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const dataView = new DataView(buffer); - expect(dataView[0]).toBe(undefined); - `); + env.evaluate(untrusted('data-view.js')); }); }); @@ -249,37 +111,15 @@ describe('FileReader', () => { endowments: Object.getOwnPropertyDescriptors({ done, expect }), }); - env.evaluate(` - const source = new Uint8Array([97, 98, 99]); - const blob = new Blob([source]); - const reader = new FileReader(); - - reader.onload = (event) => { - expect(reader.result.byteLength).toBe(source.length); - expect(reader.result).toBeInstanceOf(ArrayBuffer); - done(); - }; - reader.readAsArrayBuffer(blob); - `); + env.evaluate(untrusted('file-reader.js')); }); - it('reads from blobs created from typed arrays, when typed arrays are not remapped', (done) => { + it('reads from blobs created from typed arrays, when maxPerfMode is true', (done) => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ done, expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const source = new Uint8Array([97, 98, 99]); - const blob = new Blob([source]); - const reader = new FileReader(); - - reader.onload = (event) => { - expect(reader.result.byteLength).toBe(source.length); - expect(reader.result).toBeInstanceOf(ArrayBuffer); - done(); - }; - reader.readAsArrayBuffer(blob); - `); + env.evaluate(untrusted('file-reader.js')); }); }); @@ -291,283 +131,75 @@ describe('TypedArray', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const bigIntTypedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - ]; - for (const bigIntTypedArray of bigIntTypedArrays) { - expect(typeof bigIntTypedArray[0]).toBe('bigint'); - } - const typedArrays = [ - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(typeof typedArray[0]).toBe('number'); - } - `); + env.evaluate(untrusted('typed-array.js')); }); - it('should support in bound index access, when typed arrays are not remapped', () => { + it('should support in bound index access, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const bigIntTypedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - ]; - for (const bigIntTypedArray of bigIntTypedArrays) { - expect(typeof bigIntTypedArray[0]).toBe('bigint'); - } - const typedArrays = [ - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(typeof typedArray[0]).toBe('number'); - } - `); + env.evaluate(untrusted('typed-array.js')); }); it('should support in bound index access with modified prototypes', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - Reflect.setPrototypeOf(typedArray, { length: 0 }); - expect(typedArray[0]).toBeDefined(); - } - `); + env.evaluate(untrusted('typed-array-modified.js')); }); - it('should support in bound index access with modified prototypes, when typed arrays are not remapped', () => { + it('should support in bound index access with modified prototypes, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - Reflect.setPrototypeOf(typedArray, { length: 0 }); - expect(typedArray[0]).toBeDefined(); - } - `); + env.evaluate(untrusted('typed-array-modified.js')); }); it('should support setting in bound index values on subclasses', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const Ctors = [ - BigInt64Array, - BigUint64Array, - Float32Array, - Float64Array, - Int8Array, - Int16Array, - Int32Array, - Uint8Array, - Uint8ClampedArray, - Uint16Array, - Uint32Array, - ]; - for (const Ctor of Ctors) { - class Subclass extends Ctor { - constructor(arrayBuffer) { - super(arrayBuffer); - this[0] = this[0]; - } - } - const subclassed = new Subclass(buffer); - expect(subclassed[0]).toBeDefined(); - } - `); + env.evaluate(untrusted('typed-array-subclass.js')); }); - it('should support setting in bound index values on subclasses, when typed arrays are not remapped', () => { + it('should support setting in bound index values on subclasses, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const Ctors = [ - BigInt64Array, - BigUint64Array, - Float32Array, - Float64Array, - Int8Array, - Int16Array, - Int32Array, - Uint8Array, - Uint8ClampedArray, - Uint16Array, - Uint32Array, - ]; - for (const Ctor of Ctors) { - class Subclass extends Ctor { - constructor(arrayBuffer) { - super(arrayBuffer); - this[0] = this[0]; - } - } - const subclassed = new Subclass(buffer); - expect(subclassed[0]).toBeDefined(); - } - `); + env.evaluate(untrusted('typed-array-subclass.js')); }); it('should treat out of bound index access as undefined', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(typedArray[-1]).toBe(undefined); - expect(typedArray[1000]).toBe(undefined); - } - `); + env.evaluate(untrusted('typed-array-out-of-bound.js')); }); - it('should treat out of bound index access as undefined, when typed arrays are not remapped', () => { + it('should treat out of bound index access as undefined, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(typedArray[-1]).toBe(undefined); - expect(typedArray[1000]).toBe(undefined); - } - `); + env.evaluate(untrusted('typed-array-out-of-bound.js')); }); it('should support subarray method', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(() => typedArray.subarray(0)).not.toThrow(); - } - `); + env.evaluate(untrusted('typed-array-subarray.js')); }); - it('should support subarray method, when typed arrays are not remapped', () => { + it('should support subarray method, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const buffer = new ArrayBuffer(8); - const typedArrays = [ - new BigInt64Array(buffer), - new BigUint64Array(buffer), - new Float32Array(buffer), - new Float64Array(buffer), - new Int8Array(buffer), - new Int16Array(buffer), - new Int32Array(buffer), - new Uint8Array(buffer), - new Uint8ClampedArray(buffer), - new Uint16Array(buffer), - new Uint32Array(buffer), - ]; - for (const typedArray of typedArrays) { - expect(() => typedArray.subarray(0)).not.toThrow(); - } - `); + env.evaluate(untrusted('typed-array-subarray.js')); }); }); @@ -578,60 +210,28 @@ describe('URL', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const source = new Uint8Array([97, 98, 99]); - const blob = new Blob([source]); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-typed-array.js')); }); - it('can create a typed array blob url, when typed arrays are not remapped', () => { + it('can create a typed array blob url, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const source = new Uint8Array([97, 98, 99]); - const blob = new Blob([source]); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-typed-array.js')); }); it('can create an svg blob url', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const content = \` - - - \`; - - const blob = new Blob([content], { type: 'image/svg+xml' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-svg.js')); }); - it('can create an svg blob url, when typed arrays are not remapped', () => { + it('can create an svg blob url, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const content = \` - - - \`; - - const blob = new Blob([content], { type: 'image/svg+xml' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-svg.js')); }); it('can create an html blob url', () => { @@ -639,24 +239,14 @@ describe('URL', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const blob = new Blob(['

Hello World

'], { type: 'text/html' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-html.js')); }); - it('can create an html blob url, when typed arrays are not remapped', () => { + it('can create an html blob url, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const blob = new Blob(['

Hello World

'], { type: 'text/html' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-html.js')); }); it('can create an xml blob url', () => { @@ -664,24 +254,14 @@ describe('URL', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const blob = new Blob(['
foo
'], { type: 'text/xml' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-xml.js')); }); - it('can create an xml blob url, when typed arrays are not remapped', () => { + it('can create an xml blob url, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const blob = new Blob(['
foo
'], { type: 'text/xml' }); - expect(() => { - URL.createObjectURL(blob); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-xml.js')); }); it('can create a File blob url', () => { @@ -689,244 +269,39 @@ describe('URL', () => { endowments: Object.getOwnPropertyDescriptors({ expect }), }); - env.evaluate(` - const f = new File( - ['

PEW

'], - 'foo.txt' - ); - expect(() => { - URL.createObjectURL(f); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-file-blob.js')); }); - it('can create a File blob url, when typed arrays are not remapped', () => { + it('can create a File blob url, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); - env.evaluate(` - const f = new File( - ['

PEW

'], - 'foo.txt' - ); - expect(() => { - URL.createObjectURL(f); - }).not.toThrow(); - `); + env.evaluate(untrusted('url-file-blob.js')); }); }); describe('FileSaver library', () => { beforeEach(createEnvironmentThatAlwaysRemapsTypedArray); - /* -- Begin Library Code --*/ - /* - * FileSaver.js - * A saveAs() FileSaver implementation. - * - * By Eli Grey, http://eligrey.com - * - * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) - * source : http://purl.eligrey.com/github/FileSaver.js - */ - // The one and only way of getting global scope in all environments - // https://stackoverflow.com/q/3277182/1008999 - - const FILE_SAVER_LIBRARY_SRC = `function bom(blob, opts) { - if (typeof opts === 'undefined') opts = { - autoBom: false - };else if (typeof opts !== 'object') { - console.warn('Deprecated: Expected third argument to be a object'); - opts = { - autoBom: !opts - }; - } // prepend BOM for UTF-8 XML and text/* types (including HTML) - // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF - - if (opts.autoBom && /^\\s*(?:text\\/\\S*|application\\/xml|\\S*\\/\\S*\\+xml)\\s*;.*charset\\s*=\\s*utf-8/i.test(blob.type)) { - return new Blob([String.fromCharCode(0xFEFF), blob], { - type: blob.type - }); - } - - return blob; - } - - function download(url, name, opts) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url); - xhr.responseType = 'blob'; - - xhr.onload = function () { - saveAs(xhr.response, name, opts); - }; - - xhr.onerror = function () { - console.error('could not download file'); - }; - - xhr.send(); - } - - function corsEnabled(url) { - var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker - - xhr.open('HEAD', url, false); - - try { - xhr.send(); - } catch (e) {} - - return xhr.status >= 200 && xhr.status <= 299; - } - - - function click(node) { - try { - node.dispatchEvent(new MouseEvent('click')); - } catch (e) { - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); - node.dispatchEvent(evt); - } - } // Detect WebView inside a native macOS app by ruling out all browsers - // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too - // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos - - - var isMacOSWebView = /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); - var saveAs = window.saveAs || ( // probably in some web worker - typeof window !== 'object' || window !== window ? function saveAs() {} - /* noop */ - // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView - : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { - var URL = window.URL || window.webkitURL; - var a = document.createElement('a'); - name = name || blob.name || 'download'; - a.download = name; - a.rel = 'noopener'; // tabnabbing - // TODO: detect chrome extensions & packaged apps - // a.target = '_blank' - - if (typeof blob === 'string') { - // Support regular links - a.href = blob; - - if (a.origin !== location.origin) { - corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); - } else { - click(a); - } - } else { - // Support blobs - a.href = URL.createObjectURL(blob); - setTimeout(function () { - URL.revokeObjectURL(a.href); - }, 4E4); // 40s - - setTimeout(function () { - click(a); - }, 0); - } - } // Use msSaveOrOpenBlob as a second approach - : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { - name = name || blob.name || 'download'; - - if (typeof blob === 'string') { - if (corsEnabled(blob)) { - download(blob, name, opts); - } else { - var a = document.createElement('a'); - a.href = blob; - a.target = '_blank'; - setTimeout(function () { - click(a); - }); - } - } else { - navigator.msSaveOrOpenBlob(bom(blob, opts), name); - } - } // Fallback to using FileReader and a popup - : function saveAs(blob, name, opts, popup) { - // Open a popup immediately do go around popup blocker - // Mostly only available on user interaction and the fileReader is async so... - popup = popup || open('', '_blank'); - - if (popup) { - popup.document.title = popup.document.body.innerText = 'downloading...'; - } - - if (typeof blob === 'string') return download(blob, name, opts); - var force = blob.type === 'application/octet-stream'; - - var isSafari = /constructor/i.test(window.HTMLElement) || window.safari; - - var isChromeIOS = /CriOS\\/[\\d]+/.test(navigator.userAgent); - - if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { - // Safari doesn't allow downloading of blob URLs - var reader = new FileReader(); - - reader.onloadend = function () { - var url = reader.result; - url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); - if (popup) popup.location.href = url;else location = url; - popup = null; // reverse-tabnabbing #460 - }; - - reader.readAsDataURL(blob); - } else { - var URL = window.URL || window.webkitURL; - var url = URL.createObjectURL(blob); - if (popup) popup.location = url;else location.href = url; - popup = null; // reverse-tabnabbing #460 - - setTimeout(function () { - URL.revokeObjectURL(url); - }, 4E4); // 40s - } - }); - window.saveAs = saveAs.saveAs = saveAs; - `; - /* -- End Library Code --*/ - it('can create a blob to save as', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), }); env.evaluate(` - ${FILE_SAVER_LIBRARY_SRC} - - const blob = new Blob(['Hello, world!'], {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(blob, 'hello world.txt'); - }).not.toThrow(); - - const blobB = new Blob([new Uint8Array([97, 98, 99])], {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(blobB, 'binary.txt'); - }).not.toThrow(); + ${untrusted('FileSaver.js')} + ${untrusted('FileSaver-blob.js')} `); }); - it('can create a blob to save as, when typed arrays are not remapped', () => { + it('can create a blob to save as, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); env.evaluate(` - ${FILE_SAVER_LIBRARY_SRC} - - const blob = new Blob(['Hello, world!'], {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(blob, 'hello world.txt'); - }).not.toThrow(); - - const blobB = new Blob([new Uint8Array([97, 98, 99])], {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(blobB, 'binary.txt'); - }).not.toThrow(); + ${untrusted('FileSaver.js')} + ${untrusted('FileSaver-blob.js')} `); }); @@ -936,37 +311,19 @@ describe('FileSaver library', () => { }); env.evaluate(` - ${FILE_SAVER_LIBRARY_SRC} - - const file = new File(['Hello, world!'], 'plaintext.txt', {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(file, 'plaintext.txt'); - }).not.toThrow(); - - const fileB = new File([new Uint8Array([97, 98, 99])], 'binary.txt', {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(fileB, 'binary.txt'); - }).not.toThrow(); + ${untrusted('FileSaver.js')} + ${untrusted('FileSaver-file.js')} `); }); - it('can create a file to save as, when typed arrays are not remapped', () => { + it('can create a file to save as, when maxPerfMode is true', () => { const env = createVirtualEnvironment(window, { endowments: Object.getOwnPropertyDescriptors({ expect }), - remapTypedArrays: false, + maxPerfMode: true, }); env.evaluate(` - ${FILE_SAVER_LIBRARY_SRC} - - const file = new File(['Hello, world!'], 'plaintext.txt', {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(file, 'plaintext.txt'); - }).not.toThrow(); - - const fileB = new File([new Uint8Array([97, 98, 99])], 'binary.txt', {type: 'text/plain;charset=utf-8'}); - expect(() => { - saveAs(fileB, 'binary.txt'); - }).not.toThrow(); + ${untrusted('FileSaver.js')} + ${untrusted('FileSaver-file.js')} `); }); }); diff --git a/test/membrane/untrusted/binary-data/FileSaver-blob.js b/test/membrane/untrusted/binary-data/FileSaver-blob.js new file mode 100644 index 00000000..634e6c7b --- /dev/null +++ b/test/membrane/untrusted/binary-data/FileSaver-blob.js @@ -0,0 +1,9 @@ +const blob = new Blob(['Hello, world!'], { type: 'text/plain;charset=utf-8' }); +expect(() => { + saveAs(blob, 'hello world.txt'); +}).not.toThrow(); + +const blobB = new Blob([new Uint8Array([97, 98, 99])], { type: 'text/plain;charset=utf-8' }); +expect(() => { + saveAs(blobB, 'binary.txt'); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/FileSaver-file.js b/test/membrane/untrusted/binary-data/FileSaver-file.js new file mode 100644 index 00000000..31efa3f1 --- /dev/null +++ b/test/membrane/untrusted/binary-data/FileSaver-file.js @@ -0,0 +1,11 @@ +const file = new File(['Hello, world!'], 'plaintext.txt', { type: 'text/plain;charset=utf-8' }); +expect(() => { + saveAs(file, 'plaintext.txt'); +}).not.toThrow(); + +const fileB = new File([new Uint8Array([97, 98, 99])], 'binary.txt', { + type: 'text/plain;charset=utf-8', +}); +expect(() => { + saveAs(fileB, 'binary.txt'); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/FileSaver.js b/test/membrane/untrusted/binary-data/FileSaver.js new file mode 100644 index 00000000..64fd2500 --- /dev/null +++ b/test/membrane/untrusted/binary-data/FileSaver.js @@ -0,0 +1,172 @@ +/* +* FileSaver.js +* A saveAs() FileSaver implementation. +* +* By Eli Grey, http://eligrey.com +* +* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) +* source : http://purl.eligrey.com/github/FileSaver.js +*/ + +// The one and only way of getting global scope in all environments +// https://stackoverflow.com/q/3277182/1008999 +var _global = typeof window === 'object' && window.window === window + ? window : typeof self === 'object' && self.self === self + ? self : typeof global === 'object' && global.global === global + ? global + : this + +function bom (blob, opts) { + if (typeof opts === 'undefined') opts = { autoBom: false } + else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object') + opts = { autoBom: !opts } + } + + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type }) + } + return blob +} + +function download (url, name, opts) { + var xhr = new XMLHttpRequest() + xhr.open('GET', url) + xhr.responseType = 'blob' + xhr.onload = function () { + saveAs(xhr.response, name, opts) + } + xhr.onerror = function () { + console.error('could not download file') + } + xhr.send() +} + +function corsEnabled (url) { + var xhr = new XMLHttpRequest() + // use sync to avoid popup blocker + xhr.open('HEAD', url, false) + try { + xhr.send() + } catch (e) {} + return xhr.status >= 200 && xhr.status <= 299 +} + +// `a.click()` doesn't work for all browsers (#465) +function click (node) { + try { + node.dispatchEvent(new MouseEvent('click')) + } catch (e) { + var evt = document.createEvent('MouseEvents') + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, + 20, false, false, false, false, 0, null) + node.dispatchEvent(evt) + } +} + +// Detect WebView inside a native macOS app by ruling out all browsers +// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too +// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos +var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent) + +var saveAs = _global.saveAs || ( + // probably in some web worker + (typeof window !== 'object' || window !== _global) + ? function saveAs () { /* noop */ } + + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : ('download' in HTMLAnchorElement.prototype && !isMacOSWebView) + ? function saveAs (blob, name, opts) { + var URL = _global.URL || _global.webkitURL + // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561) + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') + name = name || blob.name || 'download' + + a.download = name + a.rel = 'noopener' // tabnabbing + + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob + if (a.origin !== location.origin) { + corsEnabled(a.href) + ? download(blob, name, opts) + : click(a, a.target = '_blank') + } else { + click(a) + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob) + setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s + setTimeout(function () { click(a) }, 0) + } + } + + // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator + ? function saveAs (blob, name, opts) { + name = name || blob.name || 'download' + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts) + } else { + var a = document.createElement('a') + a.href = blob + a.target = '_blank' + setTimeout(function () { click(a) }) + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name) + } + } + + // Fallback to using FileReader and a popup + : function saveAs (blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank') + if (popup) { + popup.document.title = + popup.document.body.innerText = 'downloading...' + } + + if (typeof blob === 'string') return download(blob, name, opts) + + var force = blob.type === 'application/octet-stream' + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent) + + if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader() + reader.onloadend = function () { + var url = reader.result + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;') + if (popup) popup.location.href = url + else location = url + popup = null // reverse-tabnabbing #460 + } + reader.readAsDataURL(blob) + } else { + var URL = _global.URL || _global.webkitURL + var url = URL.createObjectURL(blob) + if (popup) popup.location = url + else location.href = url + popup = null // reverse-tabnabbing #460 + setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s + } + } +) + +_global.saveAs = saveAs.saveAs = saveAs + +if (typeof module !== 'undefined') { + module.exports = saveAs; +} \ No newline at end of file diff --git a/test/membrane/untrusted/binary-data/always-remaps.js b/test/membrane/untrusted/binary-data/always-remaps.js new file mode 100644 index 00000000..66d79efa --- /dev/null +++ b/test/membrane/untrusted/binary-data/always-remaps.js @@ -0,0 +1,2 @@ +const u8a = new Uint8Array(); +expect(u8a instanceof OuterUint8Array).toBe(true); diff --git a/test/membrane/untrusted/binary-data/atomics.js b/test/membrane/untrusted/binary-data/atomics.js new file mode 100644 index 00000000..bf197805 --- /dev/null +++ b/test/membrane/untrusted/binary-data/atomics.js @@ -0,0 +1,11 @@ +const ab = new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT); +const i32a = new Int32Array(ab); +i32a[0] = 9; +Atomics.add(i32a, 0, 33); // 42 +Atomics.and(i32a, 0, 1); // 0 +Atomics.exchange(i32a, 0, 42); // 42 +Atomics.or(i32a, 0, 1); // 43 +Atomics.store(i32a, 0, 18); // 18 +Atomics.sub(i32a, 0, 10); +Atomics.xor(i32a, 0, 1); +expect(Atomics.load(i32a, 0)).toBe(9); diff --git a/test/membrane/untrusted/binary-data/blob.js b/test/membrane/untrusted/binary-data/blob.js new file mode 100644 index 00000000..27f6ef41 --- /dev/null +++ b/test/membrane/untrusted/binary-data/blob.js @@ -0,0 +1,6 @@ +const a = new Uint8Array([97, 98, 99]); +const b = new Blob([a], { type: 'application/octet-stream' }); +b.text().then((output) => { + expect(output).toBe('abc'); + done(); +}); diff --git a/test/membrane/untrusted/binary-data/crypto.js b/test/membrane/untrusted/binary-data/crypto.js new file mode 100644 index 00000000..b00ac50c --- /dev/null +++ b/test/membrane/untrusted/binary-data/crypto.js @@ -0,0 +1,23 @@ +expect(() => { + crypto.getRandomValues(new Uint8Array(1)); +}).not.toThrow(); +async function f() { + const algorithm = { name: 'RSA-OAEP' }; + const data = new Uint8Array([255]); + const keyPair = await window.crypto.subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['encrypt', 'decrypt'] + ); + + const encrypted = await crypto.subtle.encrypt(algorithm, keyPair.publicKey, data); + + const decrypted = await crypto.subtle.decrypt(algorithm, keyPair.privateKey, encrypted); +} + +f().then(done); diff --git a/test/membrane/untrusted/binary-data/data-view.js b/test/membrane/untrusted/binary-data/data-view.js new file mode 100644 index 00000000..9a1e6350 --- /dev/null +++ b/test/membrane/untrusted/binary-data/data-view.js @@ -0,0 +1,3 @@ +const buffer = new ArrayBuffer(8); +const dataView = new DataView(buffer); +expect(dataView[0]).toBe(undefined); diff --git a/test/membrane/untrusted/binary-data/file-reader.js b/test/membrane/untrusted/binary-data/file-reader.js new file mode 100644 index 00000000..31998582 --- /dev/null +++ b/test/membrane/untrusted/binary-data/file-reader.js @@ -0,0 +1,10 @@ +const source = new Uint8Array([97, 98, 99]); +const blob = new Blob([source]); +const reader = new FileReader(); + +reader.onload = (event) => { + expect(reader.result.byteLength).toBe(source.length); + expect(reader.result).toBeInstanceOf(ArrayBuffer); + done(); +}; +reader.readAsArrayBuffer(blob); diff --git a/test/membrane/untrusted/binary-data/typed-array-modified.js b/test/membrane/untrusted/binary-data/typed-array-modified.js new file mode 100644 index 00000000..082ce97d --- /dev/null +++ b/test/membrane/untrusted/binary-data/typed-array-modified.js @@ -0,0 +1,18 @@ +const buffer = new ArrayBuffer(8); +const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), +]; +for (const typedArray of typedArrays) { + Reflect.setPrototypeOf(typedArray, { length: 0 }); + expect(typedArray[0]).toBeDefined(); +} diff --git a/test/membrane/untrusted/binary-data/typed-array-out-of-bound.js b/test/membrane/untrusted/binary-data/typed-array-out-of-bound.js new file mode 100644 index 00000000..a873e29f --- /dev/null +++ b/test/membrane/untrusted/binary-data/typed-array-out-of-bound.js @@ -0,0 +1,18 @@ +const buffer = new ArrayBuffer(8); +const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), +]; +for (const typedArray of typedArrays) { + expect(typedArray[-1]).toBe(undefined); + expect(typedArray[1000]).toBe(undefined); +} diff --git a/test/membrane/untrusted/binary-data/typed-array-subarray.js b/test/membrane/untrusted/binary-data/typed-array-subarray.js new file mode 100644 index 00000000..8da5aff3 --- /dev/null +++ b/test/membrane/untrusted/binary-data/typed-array-subarray.js @@ -0,0 +1,17 @@ +const buffer = new ArrayBuffer(8); +const typedArrays = [ + new BigInt64Array(buffer), + new BigUint64Array(buffer), + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), +]; +for (const typedArray of typedArrays) { + expect(() => typedArray.subarray(0)).not.toThrow(); +} diff --git a/test/membrane/untrusted/binary-data/typed-array-subclass.js b/test/membrane/untrusted/binary-data/typed-array-subclass.js new file mode 100644 index 00000000..ca1ffc0e --- /dev/null +++ b/test/membrane/untrusted/binary-data/typed-array-subclass.js @@ -0,0 +1,24 @@ +const buffer = new ArrayBuffer(8); +const Ctors = [ + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + Int8Array, + Int16Array, + Int32Array, + Uint8Array, + Uint8ClampedArray, + Uint16Array, + Uint32Array, +]; +for (const Ctor of Ctors) { + class Subclass extends Ctor { + constructor(arrayBuffer) { + super(arrayBuffer); + this[0] = this[0]; + } + } + const subclassed = new Subclass(buffer); + expect(subclassed[0]).toBeDefined(); +} diff --git a/test/membrane/untrusted/binary-data/typed-array.js b/test/membrane/untrusted/binary-data/typed-array.js new file mode 100644 index 00000000..26928872 --- /dev/null +++ b/test/membrane/untrusted/binary-data/typed-array.js @@ -0,0 +1,19 @@ +const buffer = new ArrayBuffer(8); +const bigIntTypedArrays = [new BigInt64Array(buffer), new BigUint64Array(buffer)]; +for (const bigIntTypedArray of bigIntTypedArrays) { + expect(typeof bigIntTypedArray[0]).toBe('bigint'); +} +const typedArrays = [ + new Float32Array(buffer), + new Float64Array(buffer), + new Int8Array(buffer), + new Int16Array(buffer), + new Int32Array(buffer), + new Uint8Array(buffer), + new Uint8ClampedArray(buffer), + new Uint16Array(buffer), + new Uint32Array(buffer), +]; +for (const typedArray of typedArrays) { + expect(typeof typedArray[0]).toBe('number'); +} diff --git a/test/membrane/untrusted/binary-data/url-file-blob.js b/test/membrane/untrusted/binary-data/url-file-blob.js new file mode 100644 index 00000000..0a69b6f6 --- /dev/null +++ b/test/membrane/untrusted/binary-data/url-file-blob.js @@ -0,0 +1,7 @@ +const f = new File( + ['

PEW

'], + 'foo.txt' +); +expect(() => { + URL.createObjectURL(f); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/url-html.js b/test/membrane/untrusted/binary-data/url-html.js new file mode 100644 index 00000000..9a77c08c --- /dev/null +++ b/test/membrane/untrusted/binary-data/url-html.js @@ -0,0 +1,4 @@ +const blob = new Blob(['

Hello World

'], { type: 'text/html' }); +expect(() => { + URL.createObjectURL(blob); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/url-svg.js b/test/membrane/untrusted/binary-data/url-svg.js new file mode 100644 index 00000000..7c608d03 --- /dev/null +++ b/test/membrane/untrusted/binary-data/url-svg.js @@ -0,0 +1,9 @@ +const content = ` + + +`; + +const blob = new Blob([content], { type: 'image/svg+xml' }); +expect(() => { + URL.createObjectURL(blob); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/url-typed-array.js b/test/membrane/untrusted/binary-data/url-typed-array.js new file mode 100644 index 00000000..407d9dba --- /dev/null +++ b/test/membrane/untrusted/binary-data/url-typed-array.js @@ -0,0 +1,5 @@ +const source = new Uint8Array([97, 98, 99]); +const blob = new Blob([source]); +expect(() => { + URL.createObjectURL(blob); +}).not.toThrow(); diff --git a/test/membrane/untrusted/binary-data/url-xml.js b/test/membrane/untrusted/binary-data/url-xml.js new file mode 100644 index 00000000..2b1a0909 --- /dev/null +++ b/test/membrane/untrusted/binary-data/url-xml.js @@ -0,0 +1,4 @@ +const blob = new Blob(['
foo
'], { type: 'text/xml' }); +expect(() => { + URL.createObjectURL(blob); +}).not.toThrow(); diff --git a/yarn.lock b/yarn.lock index 51a93108..941a692e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7493,13 +7493,18 @@ karma-coverage@2.2.1: istanbul-reports "^3.0.5" minimatch "^3.0.4" -karma-firefox-launcher@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz#9a38cc783c579a50f3ed2a82b7386186385cfc2d" - integrity sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA== +karma-file-fixtures-preprocessor@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/karma-file-fixtures-preprocessor/-/karma-file-fixtures-preprocessor-3.0.2.tgz#8b46f5c4e925319107472821ae86c87e66192bfc" + integrity sha512-r8NNFLFpQ6xeU4Eg9EJHUSlgR4+6jxtPdUvegqAhPZkchdOh6Wn5v/zX4MwWnEafSYhABtMBShRbYPrvvHqoHg== + +karma-firefox-launcher@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz#b278a4cbffa92ab81394b1a398813847b0624a85" + integrity sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw== dependencies: is-wsl "^2.2.0" - which "^2.0.1" + which "^3.0.0" karma-jasmine@5.1.0: version "5.1.0" @@ -10253,7 +10258,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10346,7 +10360,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10360,6 +10374,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -11198,7 +11219,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11216,6 +11237,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"