diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 83265227a917a9..1ffb19b4f59a31 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -11,6 +11,8 @@ const { SafePromiseAll, SafePromisePrototypeFinally, SafeSet, + StringPrototypeStartsWith, + Symbol, TypeError, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetByteLength, @@ -94,6 +96,9 @@ const { UV_EOF } = internalBinding('uv'); const encoder = new TextEncoder(); +const kValidateChunk = Symbol('kValidateChunk'); +const kDestroyOnSyncError = Symbol('kDestroyOnSyncError'); + // Collect all negative (error) ZLIB codes and Z_NEED_DICT const ZLIB_FAILURES = new SafeSet([ ...ArrayPrototypeFilter( @@ -115,7 +120,14 @@ function handleKnownInternalErrors(cause) { case cause?.code === 'ERR_STREAM_PREMATURE_CLOSE': { return new AbortError(undefined, { cause }); } - case ZLIB_FAILURES.has(cause?.code): { + case ZLIB_FAILURES.has(cause?.code): + // Brotli decoder error codes are formatted as 'ERR_' + + // BrotliDecoderErrorString(), where the latter returns strings like + // '_ERROR_FORMAT_...', '_ERROR_ALLOC_...', '_ERROR_UNREACHABLE', etc. + // The resulting JS error codes all start with 'ERR__ERROR_'. + // Falls through + case cause?.code != null && + StringPrototypeStartsWith(cause.code, 'ERR__ERROR_'): { // eslint-disable-next-line no-restricted-syntax const error = new TypeError(undefined, { cause }); error.code = cause.code; @@ -139,9 +151,10 @@ function handleKnownInternalErrors(cause) { /** * @param {Writable} streamWritable + * @param {object} [options] * @returns {WritableStream} */ -function newWritableStreamFromStreamWritable(streamWritable) { +function newWritableStreamFromStreamWritable(streamWritable, options = kEmptyObject) { // Not using the internal/streams/utils isWritableNodeStream utility // here because it will return false if streamWritable is a Duplex // whose writable option is false. For a Duplex that is not writable, @@ -220,12 +233,26 @@ function newWritableStreamFromStreamWritable(streamWritable) { if (!streamWritable.writableObjectMode && isArrayBuffer(chunk)) { chunk = new Uint8Array(chunk); } - if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) { - backpressurePromise = PromiseWithResolvers(); - return SafePromisePrototypeFinally( - backpressurePromise.promise, () => { - backpressurePromise = undefined; - }); + try { + options[kValidateChunk]?.(chunk); + if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) { + backpressurePromise = PromiseWithResolvers(); + return SafePromisePrototypeFinally( + backpressurePromise.promise, () => { + backpressurePromise = undefined; + }); + } + } catch (error) { + // When the kDestroyOnSyncError flag is set (e.g. for + // CompressionStream), a sync throw must also destroy the + // stream so the readable side is errored too. Without this + // the readable side hangs forever. This replicates the + // TransformStream semantics: error both sides on any throw + // in the transform path. + if (options[kDestroyOnSyncError]) { + destroy(streamWritable, error); + } + throw error; } }, @@ -662,9 +689,15 @@ function newReadableWritablePairFromDuplex(duplex, options = kEmptyObject) { return { readable, writable }; } + const writableOptions = { + __proto__: null, + [kValidateChunk]: options[kValidateChunk], + [kDestroyOnSyncError]: options[kDestroyOnSyncError], + }; + const writable = isWritable(duplex) ? - newWritableStreamFromStreamWritable(duplex) : + newWritableStreamFromStreamWritable(duplex, writableOptions) : new WritableStream(); if (!isWritable(duplex)) @@ -1064,4 +1097,6 @@ module.exports = { newStreamDuplexFromReadableWritablePair, newWritableStreamFromStreamBase, newReadableStreamFromStreamBase, + kValidateChunk, + kDestroyOnSyncError, }; diff --git a/lib/internal/webstreams/compression.js b/lib/internal/webstreams/compression.js index 836c02f7341f70..7ec929d94a0da0 100644 --- a/lib/internal/webstreams/compression.js +++ b/lib/internal/webstreams/compression.js @@ -7,15 +7,28 @@ const { const { newReadableWritablePairFromDuplex, + kValidateChunk, + kDestroyOnSyncError, } = require('internal/webstreams/adapters'); const { customInspect } = require('internal/webstreams/util'); +const { + isArrayBufferView, + isSharedArrayBuffer, +} = require('internal/util/types'); + const { customInspectSymbol: kInspect, kEnumerableProperty, } = require('internal/util'); +const { + codes: { + ERR_INVALID_ARG_TYPE, + }, +} = require('internal/errors'); + const { createEnumConverter } = require('internal/webidl'); let zlib; @@ -24,6 +37,18 @@ function lazyZlib() { return zlib; } +// Per the Compression Streams spec, chunks must be BufferSource +// (ArrayBuffer or ArrayBufferView not backed by SharedArrayBuffer). +function validateBufferSourceChunk(chunk) { + if (isArrayBufferView(chunk) && isSharedArrayBuffer(chunk.buffer)) { + throw new ERR_INVALID_ARG_TYPE( + 'chunk', + ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], + chunk, + ); + } +} + const formatConverter = createEnumConverter('CompressionFormat', [ 'deflate', 'deflate-raw', @@ -62,7 +87,10 @@ class CompressionStream { this.#handle = lazyZlib().createBrotliCompress(); break; } - this.#transform = newReadableWritablePairFromDuplex(this.#handle); + this.#transform = newReadableWritablePairFromDuplex(this.#handle, { + [kValidateChunk]: validateBufferSourceChunk, + [kDestroyOnSyncError]: true, + }); } /** @@ -108,7 +136,9 @@ class DecompressionStream { }); break; case 'deflate-raw': - this.#handle = lazyZlib().createInflateRaw(); + this.#handle = lazyZlib().createInflateRaw({ + rejectGarbageAfterEnd: true, + }); break; case 'gzip': this.#handle = lazyZlib().createGunzip({ @@ -116,17 +146,14 @@ class DecompressionStream { }); break; case 'brotli': - this.#handle = lazyZlib().createBrotliDecompress(); + this.#handle = lazyZlib().createBrotliDecompress({ + rejectGarbageAfterEnd: true, + }); break; } - this.#transform = newReadableWritablePairFromDuplex(this.#handle); - - this.#handle.on('error', (err) => { - if (this.#transform?.writable && - !this.#transform.writable.locked && - typeof this.#transform.writable.abort === 'function') { - this.#transform.writable.abort(err); - } + this.#transform = newReadableWritablePairFromDuplex(this.#handle, { + [kValidateChunk]: validateBufferSourceChunk, + [kDestroyOnSyncError]: true, }); } diff --git a/test/common/wpt.js b/test/common/wpt.js index 584f3c177ab0be..8a0e4bea2ec568 100644 --- a/test/common/wpt.js +++ b/test/common/wpt.js @@ -223,6 +223,7 @@ class ResourceLoader { return { ok: true, arrayBuffer() { return data.buffer; }, + bytes() { return new Uint8Array(data); }, json() { return JSON.parse(data.toString()); }, text() { return data.toString(); }, }; @@ -721,6 +722,7 @@ class WPTRunner { // Mark the whole test as failed in wpt.fyi report. reportResult?.finish('ERROR'); this.inProgress.delete(spec); + this.report?.write(); }); await events.once(worker, 'exit').catch(() => {}); @@ -787,6 +789,9 @@ class WPTRunner { } } + // Write the report on clean exit. The report is also written + // incrementally after each spec completes (see completionCallback) + // so that results survive if the process is killed. this.report?.write(); const ran = queue.length; @@ -873,6 +878,9 @@ class WPTRunner { reportResult?.finish(); } this.inProgress.delete(spec); + // Write report incrementally so results survive even if the process + // is killed before the exit handler runs. + this.report?.write(); // Always force termination of the worker. Some tests allocate resources // that would otherwise keep it alive. this.workers.get(spec).terminate(); diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 20c7df576e73b6..fdbd8022783a22 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -11,7 +11,7 @@ See [test/wpt](../../wpt/README.md) for information on how these tests are run. Last update: - common: https://github.com/web-platform-tests/wpt/tree/dbd648158d/common -- compression: https://github.com/web-platform-tests/wpt/tree/67880a4eb8/compression +- compression: https://github.com/web-platform-tests/wpt/tree/ae05f5cb53/compression - console: https://github.com/web-platform-tests/wpt/tree/e48251b778/console - dom/abort: https://github.com/web-platform-tests/wpt/tree/dc928169ee/dom/abort - dom/events: https://github.com/web-platform-tests/wpt/tree/0a811c5161/dom/events diff --git a/test/fixtures/wpt/compression/compression-bad-chunks.any.js b/test/fixtures/wpt/compression/compression-bad-chunks.any.js new file mode 100644 index 00000000000000..9afab59e2be1db --- /dev/null +++ b/test/fixtures/wpt/compression/compression-bad-chunks.any.js @@ -0,0 +1,57 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/formats.js + +'use strict'; + +const badChunks = [ + { + name: 'undefined', + value: undefined + }, + { + name: 'null', + value: null + }, + { + name: 'numeric', + value: 3.14 + }, + { + name: 'object, not BufferSource', + value: {} + }, + { + name: 'array', + value: [65] + }, + { + name: 'SharedArrayBuffer', + // Use a getter to postpone construction so that all tests don't fail where + // SharedArrayBuffer is not yet implemented. + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer; + } + }, + { + name: 'shared Uint8Array', + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new Uint8Array(new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer) + } + }, +]; + +for (const format of formats) { + for (const chunk of badChunks) { + promise_test(async t => { + const cs = new CompressionStream(format); + const reader = cs.readable.getReader(); + const writer = cs.writable.getWriter(); + const writePromise = writer.write(chunk.value); + const readPromise = reader.read(); + await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); + await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); + }, `chunk of type ${chunk.name} should error the stream for ${format}`); + } +} diff --git a/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js b/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js deleted file mode 100644 index 2d0b5684733930..00000000000000 --- a/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js +++ /dev/null @@ -1,74 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -const badChunks = [ - { - name: 'undefined', - value: undefined - }, - { - name: 'null', - value: null - }, - { - name: 'numeric', - value: 3.14 - }, - { - name: 'object, not BufferSource', - value: {} - }, - { - name: 'array', - value: [65] - }, - { - name: 'SharedArrayBuffer', - // Use a getter to postpone construction so that all tests don't fail where - // SharedArrayBuffer is not yet implemented. - get value() { - // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` - return new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer; - } - }, - { - name: 'shared Uint8Array', - get value() { - // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` - return new Uint8Array(new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer) - } - }, -]; - -for (const chunk of badChunks) { - promise_test(async t => { - const cs = new CompressionStream('gzip'); - const reader = cs.readable.getReader(); - const writer = cs.writable.getWriter(); - const writePromise = writer.write(chunk.value); - const readPromise = reader.read(); - await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); - await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); - }, `chunk of type ${chunk.name} should error the stream for gzip`); - - promise_test(async t => { - const cs = new CompressionStream('deflate'); - const reader = cs.readable.getReader(); - const writer = cs.writable.getWriter(); - const writePromise = writer.write(chunk.value); - const readPromise = reader.read(); - await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); - await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); - }, `chunk of type ${chunk.name} should error the stream for deflate`); - - promise_test(async t => { - const cs = new CompressionStream('deflate-raw'); - const reader = cs.readable.getReader(); - const writer = cs.writable.getWriter(); - const writePromise = writer.write(chunk.value); - const readPromise = reader.read(); - await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); - await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); - }, `chunk of type ${chunk.name} should error the stream for deflate-raw`); -} diff --git a/test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js b/test/fixtures/wpt/compression/compression-constructor-error.any.js similarity index 100% rename from test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js rename to test/fixtures/wpt/compression/compression-constructor-error.any.js diff --git a/test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js b/test/fixtures/wpt/compression/compression-including-empty-chunk.any.js similarity index 52% rename from test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js rename to test/fixtures/wpt/compression/compression-including-empty-chunk.any.js index a7fd1ceb24f086..3c7a722b7c19b5 100644 --- a/test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js +++ b/test/fixtures/wpt/compression/compression-including-empty-chunk.any.js @@ -1,5 +1,7 @@ // META: global=window,worker,shadowrealm // META: script=third_party/pako/pako_inflate.min.js +// META: script=resources/decompress.js +// META: script=resources/formats.js // META: timeout=long 'use strict'; @@ -42,22 +44,13 @@ const chunkLists = [ ]; const expectedValue = new TextEncoder().encode('HelloHello'); -for (const chunkList of chunkLists) { - promise_test(async t => { - const compressedData = await compressChunkList(chunkList, 'deflate'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); - }, `the result of compressing [${chunkList}] with deflate should be 'HelloHello'`); - - promise_test(async t => { - const compressedData = await compressChunkList(chunkList, 'gzip'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); - }, `the result of compressing [${chunkList}] with gzip should be 'HelloHello'`); - - promise_test(async t => { - const compressedData = await compressChunkList(chunkList, 'deflate-raw'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); - }, `the result of compressing [${chunkList}] with deflate-raw should be 'HelloHello'`); +for (const format of formats) { + for (const chunkList of chunkLists) { + promise_test(async t => { + const compressedData = await compressChunkList(chunkList, format); + const decompressedData = await decompressDataOrPako(compressedData, format); + // check that we got the same result as our original string + assert_array_equals(expectedValue, decompressedData, 'value should match'); + }, `the result of compressing [${chunkList}] with ${format} should be 'HelloHello'`); + } } diff --git a/test/fixtures/wpt/compression/compression-large-flush-output.any.js b/test/fixtures/wpt/compression/compression-large-flush-output.any.js index 6afcb4d52875b9..bc9c553b967394 100644 --- a/test/fixtures/wpt/compression/compression-large-flush-output.any.js +++ b/test/fixtures/wpt/compression/compression-large-flush-output.any.js @@ -1,6 +1,8 @@ // META: global=window,worker,shadowrealm // META: script=third_party/pako/pako_inflate.min.js // META: script=resources/concatenate-stream.js +// META: script=resources/decompress.js +// META: script=resources/formats.js // META: timeout=long 'use strict'; @@ -21,21 +23,11 @@ const fullData = new TextEncoder().encode(JSON.stringify(Array.from({ length: 10 const data = fullData.subarray(0, 35_579); const expectedValue = data; -promise_test(async t => { - const compressedData = await compressData(data, 'deflate'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); -}, `deflate compression with large flush output`); - -promise_test(async t => { - const compressedData = await compressData(data, 'gzip'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); -}, `gzip compression with large flush output`); - -promise_test(async t => { - const compressedData = await compressData(data, 'deflate-raw'); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); -}, `deflate-raw compression with large flush output`); - +for (const format of formats) { + promise_test(async t => { + const compressedData = await compressData(data, format); + const decompressedData = await decompressDataOrPako(compressedData, format); + // check that we got the same result as our original string + assert_array_equals(decompressedData, expectedValue, 'value should match'); + }, `${format} compression with large flush output`); +} diff --git a/test/fixtures/wpt/compression/compression-multiple-chunks.any.js b/test/fixtures/wpt/compression/compression-multiple-chunks.any.js new file mode 100644 index 00000000000000..2a77df2c945249 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-multiple-chunks.any.js @@ -0,0 +1,58 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: script=resources/decompress.js +// META: script=resources/formats.js +// META: timeout=long + +'use strict'; + +// This test asserts that compressing multiple chunks should work. + +// Example: ('Hello', 3) => TextEncoder().encode('HelloHelloHello') +function makeExpectedChunk(input, numberOfChunks) { + const expectedChunk = input.repeat(numberOfChunks); + return new TextEncoder().encode(expectedChunk); +} + +// Example: ('Hello', 3, 'deflate') => compress ['Hello', 'Hello', Hello'] +async function compressMultipleChunks(input, numberOfChunks, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + const chunk = new TextEncoder().encode(input); + for (let i = 0; i < numberOfChunks; ++i) { + writer.write(chunk); + } + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +const hello = 'Hello'; + +for (const format of formats) { + for (let numberOfChunks = 2; numberOfChunks <= 16; ++numberOfChunks) { + promise_test(async t => { + const compressedData = await compressMultipleChunks(hello, numberOfChunks, format); + const decompressedData = await decompressDataOrPako(compressedData, format); + const expectedValue = makeExpectedChunk(hello, numberOfChunks); + // check that we got the same result as our original string + assert_array_equals(decompressedData, expectedValue, 'value should match'); + }, `compressing ${numberOfChunks} chunks with ${format} should work`); + } +} diff --git a/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js b/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js deleted file mode 100644 index 28a90e5ca53902..00000000000000 --- a/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js +++ /dev/null @@ -1,67 +0,0 @@ -// META: global=window,worker,shadowrealm -// META: script=third_party/pako/pako_inflate.min.js -// META: timeout=long - -'use strict'; - -// This test asserts that compressing multiple chunks should work. - -// Example: ('Hello', 3) => TextEncoder().encode('HelloHelloHello') -function makeExpectedChunk(input, numberOfChunks) { - const expectedChunk = input.repeat(numberOfChunks); - return new TextEncoder().encode(expectedChunk); -} - -// Example: ('Hello', 3, 'deflate') => compress ['Hello', 'Hello', Hello'] -async function compressMultipleChunks(input, numberOfChunks, format) { - const cs = new CompressionStream(format); - const writer = cs.writable.getWriter(); - const chunk = new TextEncoder().encode(input); - for (let i = 0; i < numberOfChunks; ++i) { - writer.write(chunk); - } - const closePromise = writer.close(); - const out = []; - const reader = cs.readable.getReader(); - let totalSize = 0; - while (true) { - const { value, done } = await reader.read(); - if (done) - break; - out.push(value); - totalSize += value.byteLength; - } - await closePromise; - const concatenated = new Uint8Array(totalSize); - let offset = 0; - for (const array of out) { - concatenated.set(array, offset); - offset += array.byteLength; - } - return concatenated; -} - -const hello = 'Hello'; - -for (let numberOfChunks = 2; numberOfChunks <= 16; ++numberOfChunks) { - promise_test(async t => { - const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'deflate'); - const expectedValue = makeExpectedChunk(hello, numberOfChunks); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); - }, `compressing ${numberOfChunks} chunks with deflate should work`); - - promise_test(async t => { - const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'gzip'); - const expectedValue = makeExpectedChunk(hello, numberOfChunks); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); - }, `compressing ${numberOfChunks} chunks with gzip should work`); - - promise_test(async t => { - const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'deflate-raw'); - const expectedValue = makeExpectedChunk(hello, numberOfChunks); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); - }, `compressing ${numberOfChunks} chunks with deflate-raw should work`); -} diff --git a/test/fixtures/wpt/compression/compression-output-length.any.js b/test/fixtures/wpt/compression/compression-output-length.any.js new file mode 100644 index 00000000000000..726c32f5ff5e82 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-output-length.any.js @@ -0,0 +1,47 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/formats.js + +'use strict'; + +// This test asserts that compressed data length is shorter than the original +// data length. If the input is extremely small, the compressed data may be +// larger than the original data. + +const LARGE_FILE = '/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm'; + +async function compressArrayBuffer(input, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(input); + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +for (const format of formats) { + promise_test(async () => { + const response = await fetch(LARGE_FILE); + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const originalLength = bufferView.length; + const compressedData = await compressArrayBuffer(bufferView, format); + const compressedLength = compressedData.length; + assert_less_than(compressedLength, originalLength, 'output should be smaller'); + }, `the length of ${format} data should be shorter than that of the original data`); +} diff --git a/test/fixtures/wpt/compression/compression-output-length.tentative.any.js b/test/fixtures/wpt/compression/compression-output-length.tentative.any.js deleted file mode 100644 index 7aa13734500d26..00000000000000 --- a/test/fixtures/wpt/compression/compression-output-length.tentative.any.js +++ /dev/null @@ -1,64 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -// This test asserts that compressed data length is shorter than the original -// data length. If the input is extremely small, the compressed data may be -// larger than the original data. - -const LARGE_FILE = '/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm'; - -async function compressArrayBuffer(input, format) { - const cs = new CompressionStream(format); - const writer = cs.writable.getWriter(); - writer.write(input); - const closePromise = writer.close(); - const out = []; - const reader = cs.readable.getReader(); - let totalSize = 0; - while (true) { - const { value, done } = await reader.read(); - if (done) - break; - out.push(value); - totalSize += value.byteLength; - } - await closePromise; - const concatenated = new Uint8Array(totalSize); - let offset = 0; - for (const array of out) { - concatenated.set(array, offset); - offset += array.byteLength; - } - return concatenated; -} - -promise_test(async () => { - const response = await fetch(LARGE_FILE); - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const originalLength = bufferView.length; - const compressedData = await compressArrayBuffer(bufferView, 'deflate'); - const compressedLength = compressedData.length; - assert_less_than(compressedLength, originalLength, 'output should be smaller'); -}, 'the length of deflated data should be shorter than that of the original data'); - -promise_test(async () => { - const response = await fetch(LARGE_FILE); - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const originalLength = bufferView.length; - const compressedData = await compressArrayBuffer(bufferView, 'gzip'); - const compressedLength = compressedData.length; - assert_less_than(compressedLength, originalLength, 'output should be smaller'); -}, 'the length of gzipped data should be shorter than that of the original data'); - -promise_test(async () => { - const response = await fetch(LARGE_FILE); - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const originalLength = bufferView.length; - const compressedData = await compressArrayBuffer(bufferView, 'deflate-raw'); - const compressedLength = compressedData.length; - assert_less_than(compressedLength, originalLength, 'output should be smaller'); -}, 'the length of deflated (with -raw) data should be shorter than that of the original data'); diff --git a/test/fixtures/wpt/compression/compression-stream.any.js b/test/fixtures/wpt/compression/compression-stream.any.js new file mode 100644 index 00000000000000..02183da0c63853 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-stream.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: script=resources/decompress.js +// META: script=resources/formats.js +// META: timeout=long + +'use strict'; + +const SMALL_FILE = "/media/foo.vtt"; +const LARGE_FILE = "/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm"; + +let dataPromiseList = [ + ["empty data", Promise.resolve(new Uint8Array(0))], + ["small amount data", fetch(SMALL_FILE).then(response => response.bytes())], + ["large amount data", fetch(LARGE_FILE).then(response => response.bytes())], +]; + +async function compressArrayBuffer(input, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(input); + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +test(() => { + assert_throws_js(TypeError, () => { + const transformer = new CompressionStream("nonvalid"); + }, "non supported format should throw"); +}, "CompressionStream constructor should throw on invalid format"); + +for (const format of formats) { + for (const [label, dataPromise] of dataPromiseList) { + promise_test(async () => { + const bufferView = await dataPromise; + const compressedData = await compressArrayBuffer(bufferView, format); + const decompressedData = await decompressDataOrPako(compressedData, format); + // check that we got the same result as our original string + assert_array_equals(decompressedData, bufferView, 'value should match'); + }, `${format} ${label} should be reinflated back to its origin`); + } +} diff --git a/test/fixtures/wpt/compression/compression-stream.tentative.any.js b/test/fixtures/wpt/compression/compression-stream.tentative.any.js deleted file mode 100644 index a7ea0cb908402f..00000000000000 --- a/test/fixtures/wpt/compression/compression-stream.tentative.any.js +++ /dev/null @@ -1,91 +0,0 @@ -// META: global=window,worker,shadowrealm -// META: script=third_party/pako/pako_inflate.min.js -// META: timeout=long - -'use strict'; - -const SMALL_FILE = "/media/foo.vtt"; -const LARGE_FILE = "/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm"; - -async function compressArrayBuffer(input, format) { - const cs = new CompressionStream(format); - const writer = cs.writable.getWriter(); - writer.write(input); - const closePromise = writer.close(); - const out = []; - const reader = cs.readable.getReader(); - let totalSize = 0; - while (true) { - const { value, done } = await reader.read(); - if (done) - break; - out.push(value); - totalSize += value.byteLength; - } - await closePromise; - const concatenated = new Uint8Array(totalSize); - let offset = 0; - for (const array of out) { - concatenated.set(array, offset); - offset += array.byteLength; - } - return concatenated; -} - -test(() => { - assert_throws_js(TypeError, () => { - const transformer = new CompressionStream("nonvalid"); - }, "non supported format should throw"); -}, "CompressionStream constructor should throw on invalid format"); - -promise_test(async () => { - const buffer = new ArrayBuffer(0); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "deflate"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "deflated empty data should be reinflated back to its origin"); - -promise_test(async () => { - const response = await fetch(SMALL_FILE) - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "deflate"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "deflated small amount data should be reinflated back to its origin"); - -promise_test(async () => { - const response = await fetch(LARGE_FILE) - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "deflate"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "deflated large amount data should be reinflated back to its origin"); - -promise_test(async () => { - const buffer = new ArrayBuffer(0); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "gzip"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "gzipped empty data should be reinflated back to its origin"); - -promise_test(async () => { - const response = await fetch(SMALL_FILE) - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "gzip"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "gzipped small amount data should be reinflated back to its origin"); - -promise_test(async () => { - const response = await fetch(LARGE_FILE) - const buffer = await response.arrayBuffer(); - const bufferView = new Uint8Array(buffer); - const compressedData = await compressArrayBuffer(bufferView, "gzip"); - // decompress with pako, and check that we got the same result as our original string - assert_array_equals(bufferView, pako.inflate(compressedData)); -}, "gzipped large amount data should be reinflated back to its origin"); diff --git a/test/fixtures/wpt/compression/compression-with-detach.tentative.window.js b/test/fixtures/wpt/compression/compression-with-detach.window.js similarity index 100% rename from test/fixtures/wpt/compression/compression-with-detach.tentative.window.js rename to test/fixtures/wpt/compression/compression-with-detach.window.js diff --git a/test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js b/test/fixtures/wpt/compression/decompression-bad-chunks.any.js similarity index 82% rename from test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js rename to test/fixtures/wpt/compression/decompression-bad-chunks.any.js index f450b0c4cb2553..57adebb2837228 100644 --- a/test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js +++ b/test/fixtures/wpt/compression/decompression-bad-chunks.any.js @@ -1,4 +1,5 @@ // META: global=window,worker,shadowrealm +// META: script=resources/formats.js 'use strict'; @@ -70,16 +71,10 @@ async function decompress(chunk, format, t) await promise_rejects_js(t, TypeError, reader.closed, 'read.closed should reject'); } -for (const chunk of badChunks) { - promise_test(async t => { - await decompress(chunk, 'gzip', t); - }, `chunk of type ${chunk.name} should error the stream for gzip`); - - promise_test(async t => { - await decompress(chunk, 'deflate', t); - }, `chunk of type ${chunk.name} should error the stream for deflate`); - - promise_test(async t => { - await decompress(chunk, 'deflate-raw', t); - }, `chunk of type ${chunk.name} should error the stream for deflate-raw`); +for (const format of formats) { + for (const chunk of badChunks) { + promise_test(async t => { + await decompress(chunk, format, t); + }, `chunk of type ${chunk.name} should error the stream for ${format}`); + } } diff --git a/test/fixtures/wpt/compression/decompression-buffersource.any.js b/test/fixtures/wpt/compression/decompression-buffersource.any.js new file mode 100644 index 00000000000000..56216c8bd62372 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-buffersource.any.js @@ -0,0 +1,86 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const compressedBytes = [ + ["deflate", [120, 156, 75, 52, 48, 52, 50, 54, 49, 53, 3, 0, 8, 136, 1, 199]], + ["gzip", [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 52, 48, 52, 2, 0, 216, 252, 63, 136, 4, 0, 0, 0]], + ["deflate-raw", [ + 0x00, 0x06, 0x00, 0xf9, 0xff, 0x41, 0x42, 0x43, + 0x44, 0x45, 0x46, 0x01, 0x00, 0x00, 0xff, 0xff, + ]], + ["brotli", [0x21, 0x08, 0x00, 0x04, 0x66, 0x6F, 0x6F, 0x03]] +]; +// These chunk values below were chosen to make the length of the compressed +// output be a multiple of 8 bytes. +const expectedChunkValue = new Map(Object.entries({ + "deflate": new TextEncoder().encode('a0123456'), + "gzip": new TextEncoder().encode('a012'), + "deflate-raw": new TextEncoder().encode('ABCDEF'), + "brotli": new TextEncoder().encode('foo'), +})); + +const bufferSourceChunks = compressedBytes.map(([format, bytes]) => [format, [ + { + name: 'ArrayBuffer', + value: new Uint8Array(bytes).buffer + }, + { + name: 'Int8Array', + value: new Int8Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Uint8Array', + value: new Uint8Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Uint8ClampedArray', + value: new Uint8ClampedArray(new Uint8Array(bytes).buffer) + }, + { + name: 'Int16Array', + value: new Int16Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Uint16Array', + value: new Uint16Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Int32Array', + value: new Int32Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Uint32Array', + value: new Uint32Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Float16Array', + value: () => new Float16Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Float32Array', + value: new Float32Array(new Uint8Array(bytes).buffer) + }, + { + name: 'Float64Array', + value: new Float64Array(new Uint8Array(bytes).buffer) + }, + { + name: 'DataView', + value: new DataView(new Uint8Array(bytes).buffer) + }, +]]); + +for (const [format, chunks] of bufferSourceChunks) { + for (const chunk of chunks) { + promise_test(async t => { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(typeof chunk.value === 'function' ? chunk.value() : chunk.value); + writer.close(); + const { value } = await reader.read(); + assert_array_equals(Array.from(value), expectedChunkValue.get(format), 'value should match'); + }, `chunk of type ${chunk.name} should work for ${format}`); + } +} diff --git a/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js b/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js deleted file mode 100644 index f4316ba1fc876e..00000000000000 --- a/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js +++ /dev/null @@ -1,204 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -const compressedBytesWithDeflate = [120, 156, 75, 52, 48, 52, 50, 54, 49, 53, 3, 0, 8, 136, 1, 199]; -const compressedBytesWithGzip = [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 52, 48, 52, 2, 0, 216, 252, 63, 136, 4, 0, 0, 0]; -const compressedBytesWithDeflateRaw = [ - 0x00, 0x06, 0x00, 0xf9, 0xff, 0x41, 0x42, 0x43, - 0x44, 0x45, 0x46, 0x01, 0x00, 0x00, 0xff, 0xff, -]; -// These chunk values below were chosen to make the length of the compressed -// output be a multiple of 8 bytes. -const deflateExpectedChunkValue = new TextEncoder().encode('a0123456'); -const gzipExpectedChunkValue = new TextEncoder().encode('a012'); -const deflateRawExpectedChunkValue = new TextEncoder().encode('ABCDEF'); - -const bufferSourceChunksForDeflate = [ - { - name: 'ArrayBuffer', - value: new Uint8Array(compressedBytesWithDeflate).buffer - }, - { - name: 'Int8Array', - value: new Int8Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Uint8Array', - value: new Uint8Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Uint8ClampedArray', - value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Int16Array', - value: new Int16Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Uint16Array', - value: new Uint16Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Int32Array', - value: new Int32Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Uint32Array', - value: new Uint32Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Float16Array', - value: () => new Float16Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Float32Array', - value: new Float32Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'Float64Array', - value: new Float64Array(new Uint8Array(compressedBytesWithDeflate).buffer) - }, - { - name: 'DataView', - value: new DataView(new Uint8Array(compressedBytesWithDeflate).buffer) - }, -]; - -const bufferSourceChunksForGzip = [ - { - name: 'ArrayBuffer', - value: new Uint8Array(compressedBytesWithGzip).buffer - }, - { - name: 'Int8Array', - value: new Int8Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Uint8Array', - value: new Uint8Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Uint8ClambedArray', - value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Int16Array', - value: new Int16Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Uint16Array', - value: new Uint16Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Int32Array', - value: new Int32Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Uint32Array', - value: new Uint32Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Float16Array', - value: () => new Float16Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Float32Array', - value: new Float32Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'Float64Array', - value: new Float64Array(new Uint8Array(compressedBytesWithGzip).buffer) - }, - { - name: 'DataView', - value: new DataView(new Uint8Array(compressedBytesWithGzip).buffer) - }, -]; - -const bufferSourceChunksForDeflateRaw = [ - { - name: 'ArrayBuffer', - value: new Uint8Array(compressedBytesWithDeflateRaw).buffer - }, - { - name: 'Int8Array', - value: new Int8Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Uint8Array', - value: new Uint8Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Uint8ClampedArray', - value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Int16Array', - value: new Int16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Uint16Array', - value: new Uint16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Int32Array', - value: new Int32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Uint32Array', - value: new Uint32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Float16Array', - value: () => new Float16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Float32Array', - value: new Float32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'Float64Array', - value: new Float64Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, - { - name: 'DataView', - value: new DataView(new Uint8Array(compressedBytesWithDeflateRaw).buffer) - }, -]; - -for (const chunk of bufferSourceChunksForDeflate) { - promise_test(async t => { - const ds = new DecompressionStream('deflate'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(typeof chunk.value === 'function' ? chunk.value() : chunk.value); - writer.close(); - const { value } = await reader.read(); - assert_array_equals(Array.from(value), deflateExpectedChunkValue, 'value should match'); - }, `chunk of type ${chunk.name} should work for deflate`); -} - -for (const chunk of bufferSourceChunksForGzip) { - promise_test(async t => { - const ds = new DecompressionStream('gzip'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(typeof chunk.value === 'function' ? chunk.value() : chunk.value); - writer.close(); - const { value } = await reader.read(); - assert_array_equals(Array.from(value), gzipExpectedChunkValue, 'value should match'); - }, `chunk of type ${chunk.name} should work for gzip`); -} - -for (const chunk of bufferSourceChunksForDeflateRaw) { - promise_test(async t => { - const ds = new DecompressionStream('deflate-raw'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(typeof chunk.value === 'function' ? chunk.value() : chunk.value); - writer.close(); - const { value } = await reader.read(); - assert_array_equals(Array.from(value), deflateRawExpectedChunkValue, 'value should match'); - }, `chunk of type ${chunk.name} should work for deflate-raw`); -} diff --git a/test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js b/test/fixtures/wpt/compression/decompression-constructor-error.any.js similarity index 100% rename from test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js rename to test/fixtures/wpt/compression/decompression-constructor-error.any.js diff --git a/test/fixtures/wpt/compression/decompression-correct-input.any.js b/test/fixtures/wpt/compression/decompression-correct-input.any.js new file mode 100644 index 00000000000000..0d0d82e714a882 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-correct-input.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/decompression-input.js + +'use strict'; + +for (const [format, chunk] of compressedBytes) { + promise_test(async t => { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(chunk); + const { done, value } = await reader.read(); + assert_array_equals(Array.from(value), expectedChunkValue, "value should match"); + }, `decompressing ${format} input should work`); +} diff --git a/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js deleted file mode 100644 index 90519445e3667b..00000000000000 --- a/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js +++ /dev/null @@ -1,39 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); -const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); -const deflateRawChunkValue = new Uint8Array([ - 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, - 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, -]); -const trueChunkValue = new TextEncoder().encode('expected output'); - -promise_test(async t => { - const ds = new DecompressionStream('deflate'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(deflateChunkValue); - const { done, value } = await reader.read(); - assert_array_equals(Array.from(value), trueChunkValue, "value should match"); -}, 'decompressing deflated input should work'); - - -promise_test(async t => { - const ds = new DecompressionStream('gzip'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(gzipChunkValue); - const { done, value } = await reader.read(); - assert_array_equals(Array.from(value), trueChunkValue, "value should match"); -}, 'decompressing gzip input should work'); - -promise_test(async t => { - const ds = new DecompressionStream('deflate-raw'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(deflateRawChunkValue); - const { done, value } = await reader.read(); - assert_array_equals(Array.from(value), trueChunkValue, "value should match"); -}, 'decompressing deflated (with -raw) input should work'); diff --git a/test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-corrupt-input.any.js similarity index 93% rename from test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js rename to test/fixtures/wpt/compression/decompression-corrupt-input.any.js index fc18197dfbd3db..492232ee92b777 100644 --- a/test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js +++ b/test/fixtures/wpt/compression/decompression-corrupt-input.any.js @@ -1,4 +1,5 @@ -// META global=window,worker,shadowrealm +// META: global=window,worker,shadowrealm +// META: script=resources/decompression-input.js // This test checks that DecompressionStream behaves according to the standard // when the input is corrupted. To avoid a combinatorial explosion in the @@ -13,8 +14,7 @@ const expectations = [ format: 'deflate', // Decompresses to 'expected output'. - baseInput: [120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, - 40, 45, 1, 0, 48, 173, 6, 36], + baseInput: deflateChunkValue, // See RFC1950 for the definition of the various fields used by deflate: // https://tools.ietf.org/html/rfc1950. @@ -102,9 +102,7 @@ const expectations = [ format: 'gzip', // Decompresses to 'expected output'. - baseInput: [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, - 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, - 0, 0], + baseInput: gzipChunkValue, // See RFC1952 for the definition of the various fields used by gzip: // https://tools.ietf.org/html/rfc1952. @@ -224,6 +222,14 @@ const expectations = [ ] } ] + }, + { + format: 'brotli', + + // Decompresses to 'expected output'. + baseInput: brotliChunkValue, + + fields: [] } ]; @@ -274,18 +280,18 @@ function corruptInput(input, offset, length, value) { for (const { format, baseInput, fields } of expectations) { promise_test(async () => { - const { result } = await tryDecompress(new Uint8Array(baseInput), format); + const { result } = await tryDecompress(baseInput, format); assert_equals(result, 'success', 'decompression should succeed'); }, `the unchanged input for '${format}' should decompress successfully`); promise_test(async () => { - const truncatedInput = new Uint8Array(baseInput.slice(0, -1)); + const truncatedInput = baseInput.subarray(0, -1); const { result } = await tryDecompress(truncatedInput, format); assert_equals(result, 'error', 'decompression should fail'); }, `truncating the input for '${format}' should give an error`); promise_test(async () => { - const extendedInput = new Uint8Array(baseInput.concat([0])); + const extendedInput = new Uint8Array([...baseInput, 0]); const { result } = await tryDecompress(extendedInput, format); assert_equals(result, 'error', 'decompression should fail'); }, `trailing junk for '${format}' should give an error`); diff --git a/test/fixtures/wpt/compression/decompression-empty-input.any.js b/test/fixtures/wpt/compression/decompression-empty-input.any.js new file mode 100644 index 00000000000000..40a660dbe02898 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-empty-input.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const emptyValues = [ + ["gzip", new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0])], + ["deflate", new Uint8Array([120, 156, 3, 0, 0, 0, 0, 1])], + ["deflate-raw", new Uint8Array([1, 0, 0, 255, 255])], + ["brotli", new Uint8Array([0xa1, 0x01])], +]; + +for (const [format, emptyValue] of emptyValues) { + promise_test(async t => { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(emptyValue); + writer.close(); + const { value, done } = await reader.read(); + assert_true(done, "read() should set done"); + assert_equals(value, undefined, "value should be undefined"); + await writePromise; + }, `decompressing ${format} empty input should work`); +} diff --git a/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js deleted file mode 100644 index 201db8ec0b0d7c..00000000000000 --- a/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js +++ /dev/null @@ -1,43 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -const gzipEmptyValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]); -const deflateEmptyValue = new Uint8Array([120, 156, 3, 0, 0, 0, 0, 1]); -const deflateRawEmptyValue = new Uint8Array([1, 0, 0, 255, 255]); - -promise_test(async t => { - const ds = new DecompressionStream('gzip'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(gzipEmptyValue); - writer.close(); - const { value, done } = await reader.read(); - assert_true(done, "read() should set done"); - assert_equals(value, undefined, "value should be undefined"); - await writePromise; -}, 'decompressing gzip empty input should work'); - -promise_test(async t => { - const ds = new DecompressionStream('deflate'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(deflateEmptyValue); - writer.close(); - const { value, done } = await reader.read(); - assert_true(done, "read() should set done"); - assert_equals(value, undefined, "value should be undefined"); - await writePromise; -}, 'decompressing deflate empty input should work'); - -promise_test(async t => { - const ds = new DecompressionStream('deflate-raw'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(deflateRawEmptyValue); - writer.close(); - const { value, done } = await reader.read(); - assert_true(done, "read() should set done"); - assert_equals(value, undefined, "value should be undefined"); - await writePromise; -}, 'decompressing deflate-raw empty input should work'); diff --git a/test/fixtures/wpt/compression/decompression-extra-input.any.js b/test/fixtures/wpt/compression/decompression-extra-input.any.js new file mode 100644 index 00000000000000..e2c6a10a718516 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-extra-input.any.js @@ -0,0 +1,20 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/decompression-input.js + +'use strict'; + +const tests = compressedBytes.map( + ([format, chunk]) => [format, new Uint8Array([...chunk, 0])] +); + +for (const [format, chunk] of tests) { + promise_test(async t => { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + writer.write(chunk).catch(() => { }); + const { done, value } = await reader.read(); + assert_array_equals(Array.from(value), expectedChunkValue, "value should match"); + await promise_rejects_js(t, TypeError, reader.read(), "Extra input should eventually throw"); + }, `decompressing ${format} input with extra pad should still give the output`); +} diff --git a/test/fixtures/wpt/compression/decompression-split-chunk.any.js b/test/fixtures/wpt/compression/decompression-split-chunk.any.js new file mode 100644 index 00000000000000..0408bfd9cd4336 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-split-chunk.any.js @@ -0,0 +1,38 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/decompression-input.js + +'use strict'; + +async function decompressArrayBuffer(input, format, chunkSize) { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + for (let beginning = 0; beginning < input.length; beginning += chunkSize) { + writer.write(input.slice(beginning, beginning + chunkSize)); + } + writer.close(); + const out = []; + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + out.push(value); + totalSize += value.byteLength; + } + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +for (const [format, bytes] of compressedBytes) { + for (let chunkSize = 1; chunkSize < 16; ++chunkSize) { + promise_test(async t => { + const decompressedData = await decompressArrayBuffer(bytes, format, chunkSize); + assert_array_equals(decompressedData, expectedChunkValue, "value should match"); + }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in ${format}`); + } +} diff --git a/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js b/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js deleted file mode 100644 index eb12c2a2360cd9..00000000000000 --- a/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js +++ /dev/null @@ -1,53 +0,0 @@ -// META: global=window,worker,shadowrealm - -'use strict'; - -const compressedBytesWithDeflate = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); -const compressedBytesWithGzip = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); -const compressedBytesWithDeflateRaw = new Uint8Array([ - 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, - 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, -]); -const expectedChunkValue = new TextEncoder().encode('expected output'); - -async function decompressArrayBuffer(input, format, chunkSize) { - const ds = new DecompressionStream(format); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - for (let beginning = 0; beginning < input.length; beginning += chunkSize) { - writer.write(input.slice(beginning, beginning + chunkSize)); - } - writer.close(); - const out = []; - let totalSize = 0; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - out.push(value); - totalSize += value.byteLength; - } - const concatenated = new Uint8Array(totalSize); - let offset = 0; - for (const array of out) { - concatenated.set(array, offset); - offset += array.byteLength; - } - return concatenated; -} - -for (let chunkSize = 1; chunkSize < 16; ++chunkSize) { - promise_test(async t => { - const decompressedData = await decompressArrayBuffer(compressedBytesWithDeflate, 'deflate', chunkSize); - assert_array_equals(decompressedData, expectedChunkValue, "value should match"); - }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in deflate`); - - promise_test(async t => { - const decompressedData = await decompressArrayBuffer(compressedBytesWithGzip, 'gzip', chunkSize); - assert_array_equals(decompressedData, expectedChunkValue, "value should match"); - }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in gzip`); - - promise_test(async t => { - const decompressedData = await decompressArrayBuffer(compressedBytesWithDeflateRaw, 'deflate-raw', chunkSize); - assert_array_equals(decompressedData, expectedChunkValue, "value should match"); - }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in deflate-raw`); -} diff --git a/test/fixtures/wpt/compression/decompression-uint8array-output.any.js b/test/fixtures/wpt/compression/decompression-uint8array-output.any.js new file mode 100644 index 00000000000000..31e25fa2c6fb86 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-uint8array-output.any.js @@ -0,0 +1,16 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/decompression-input.js + +'use strict'; + +for (const [format, chunkValue] of compressedBytes) { + promise_test(async t => { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(chunkValue); + const { value } = await reader.read(); + assert_equals(value.constructor, Uint8Array, "type should match"); + await writePromise; + }, `decompressing ${format} output should give Uint8Array chunks`); +} diff --git a/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js b/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js deleted file mode 100644 index 0c45a0aaa727f1..00000000000000 --- a/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js +++ /dev/null @@ -1,30 +0,0 @@ -// META: global=window,worker,shadowrealm -// META: timeout=long -// -// This test isn't actually slow usually, but sometimes it takes >10 seconds on -// Firefox with service worker for no obvious reason. - -'use strict'; - -const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); -const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); - -promise_test(async t => { - const ds = new DecompressionStream('deflate'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(deflateChunkValue); - const { value } = await reader.read(); - assert_equals(value.constructor, Uint8Array, "type should match"); - await writePromise; -}, 'decompressing deflated output should give Uint8Array chunks'); - -promise_test(async t => { - const ds = new DecompressionStream('gzip'); - const reader = ds.readable.getReader(); - const writer = ds.writable.getWriter(); - const writePromise = writer.write(gzipChunkValue); - const { value } = await reader.read(); - assert_equals(value.constructor, Uint8Array, "type should match"); - await writePromise; -}, 'decompressing gzip output should give Uint8Array chunks'); diff --git a/test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js b/test/fixtures/wpt/compression/decompression-with-detach.window.js similarity index 100% rename from test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js rename to test/fixtures/wpt/compression/decompression-with-detach.window.js diff --git a/test/fixtures/wpt/compression/resources/decompress.js b/test/fixtures/wpt/compression/resources/decompress.js new file mode 100644 index 00000000000000..ae7d1b6e4ec2bc --- /dev/null +++ b/test/fixtures/wpt/compression/resources/decompress.js @@ -0,0 +1,31 @@ +/** + * @param {Uint8Array} chunk + * @param {string} format + */ +async function decompressData(chunk, format) { + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + writer.write(chunk); + writer.close(); + const decompressedChunkList = await Array.fromAsync(ds.readable); + const mergedBlob = new Blob(decompressedChunkList); + return await mergedBlob.bytes(); +} + +/** + * @param {Uint8Array} chunk + * @param {string} format + */ +async function decompressDataOrPako(chunk, format) { + // Keep using pako for zlib to preserve existing test behavior + if (["deflate", "gzip"].includes(format)) { + return pako.inflate(chunk); + } + if (format === "deflate-raw") { + return pako.inflateRaw(chunk); + } + + // Use DecompressionStream for any newer formats, assuming implementations + // always implement decompression if they implement compression. + return decompressData(chunk, format); +} diff --git a/test/fixtures/wpt/compression/resources/decompression-input.js b/test/fixtures/wpt/compression/resources/decompression-input.js new file mode 100644 index 00000000000000..ef0acfb9ced799 --- /dev/null +++ b/test/fixtures/wpt/compression/resources/decompression-input.js @@ -0,0 +1,20 @@ +const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); +const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); +const deflateRawChunkValue = new Uint8Array([ + 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, + 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, +]); +const brotliChunkValue = new Uint8Array([ + 0x21, 0x38, 0x00, 0x04, 0x65, 0x78, 0x70, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x20, 0x6F, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x03 +]); + +const compressedBytes = [ + ["deflate", deflateChunkValue], + ["gzip", gzipChunkValue], + ["deflate-raw", deflateRawChunkValue], + ["brotli", brotliChunkValue], +]; + +const expectedChunkValue = new TextEncoder().encode('expected output'); diff --git a/test/fixtures/wpt/compression/resources/formats.js b/test/fixtures/wpt/compression/resources/formats.js new file mode 100644 index 00000000000000..efbf7c08e580aa --- /dev/null +++ b/test/fixtures/wpt/compression/resources/formats.js @@ -0,0 +1,6 @@ +const formats = [ + "deflate", + "deflate-raw", + "gzip", + "brotli", +] diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index db5222f4fefdca..0717125cb0a4b0 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -4,7 +4,7 @@ "path": "common" }, "compression": { - "commit": "67880a4eb83ca9aa732eec4b35a1971ff5bf37ff", + "commit": "ae05f5cb53d3e290b91ae1e92069baa188292880", "path": "compression" }, "console": { diff --git a/test/parallel/test-webstreams-adapters-sync-write-error.js b/test/parallel/test-webstreams-adapters-sync-write-error.js new file mode 100644 index 00000000000000..748f682365ee93 --- /dev/null +++ b/test/parallel/test-webstreams-adapters-sync-write-error.js @@ -0,0 +1,79 @@ +'use strict'; +// Flags: --no-warnings --expose-internals +require('../common'); +const assert = require('assert'); +const test = require('node:test'); +const { Duplex, Writable } = require('stream'); +const { + newWritableStreamFromStreamWritable, + newReadableWritablePairFromDuplex, +} = require('internal/webstreams/adapters'); + +// Verify that when the underlying Node.js stream throws synchronously from +// write(), the writable web stream properly rejects but does not destroy +// the stream (destroy-on-sync-throw is only used internally by +// CompressionStream/DecompressionStream). + +test('WritableStream from Node.js stream handles sync write throw', async () => { + const error = new TypeError('invalid chunk'); + const writable = new Writable({ + write() { + throw error; + }, + }); + + const ws = newWritableStreamFromStreamWritable(writable); + const writer = ws.getWriter(); + + await assert.rejects(writer.write('bad'), (err) => { + assert.strictEqual(err, error); + return true; + }); + + // Standalone writable should not be destroyed on sync write error + assert.strictEqual(writable.destroyed, false); +}); + +test('Duplex-backed pair does NOT destroy on sync write throw', async () => { + const error = new TypeError('invalid chunk'); + const duplex = new Duplex({ + read() {}, + write() { + throw error; + }, + }); + + const { writable, readable } = newReadableWritablePairFromDuplex(duplex); + const writer = writable.getWriter(); + + await assert.rejects(writer.write('bad'), (err) => { + assert.strictEqual(err, error); + return true; + }); + + // A plain Duplex should NOT be destroyed on sync write error + assert.strictEqual(duplex.destroyed, false); + + // The readable side should still be usable + const reader = readable.getReader(); + reader.cancel(); +}); + +test('WritableStream from Node.js stream - valid writes still work', async () => { + const chunks = []; + const writable = new Writable({ + write(chunk, _encoding, cb) { + chunks.push(chunk); + cb(); + }, + }); + + const ws = newWritableStreamFromStreamWritable(writable); + const writer = ws.getWriter(); + + await writer.write(Buffer.from('hello')); + await writer.write(Buffer.from(' world')); + await writer.close(); + + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); +}); diff --git a/test/parallel/test-webstreams-compression-bad-chunks.js b/test/parallel/test-webstreams-compression-bad-chunks.js new file mode 100644 index 00000000000000..4a8ca3cff8a27f --- /dev/null +++ b/test/parallel/test-webstreams-compression-bad-chunks.js @@ -0,0 +1,75 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const test = require('node:test'); +const { CompressionStream, DecompressionStream } = require('stream/web'); + +// Verify that writing invalid (non-BufferSource) chunks to +// CompressionStream and DecompressionStream properly rejects +// on both the write and the read side, instead of hanging. + +const badChunks = [ + { name: 'undefined', value: undefined, code: 'ERR_INVALID_ARG_TYPE' }, + { name: 'null', value: null, code: 'ERR_STREAM_NULL_VALUES' }, + { name: 'number', value: 3.14, code: 'ERR_INVALID_ARG_TYPE' }, + { name: 'object', value: {}, code: 'ERR_INVALID_ARG_TYPE' }, + { name: 'array', value: [65], code: 'ERR_INVALID_ARG_TYPE' }, + { + name: 'SharedArrayBuffer', + value: new SharedArrayBuffer(1), + code: 'ERR_INVALID_ARG_TYPE', + }, + { + name: 'Uint8Array backed by SharedArrayBuffer', + value: new Uint8Array(new SharedArrayBuffer(1)), + code: 'ERR_INVALID_ARG_TYPE', + }, +]; + +for (const format of ['deflate', 'deflate-raw', 'gzip', 'brotli']) { + for (const { name, value, code } of badChunks) { + const expected = { name: 'TypeError', code }; + + test(`CompressionStream rejects bad chunk (${name}) for ${format}`, async () => { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + const reader = cs.readable.getReader(); + + const writePromise = writer.write(value); + const readPromise = reader.read(); + + await assert.rejects(writePromise, expected); + await assert.rejects(readPromise, expected); + }); + + test(`DecompressionStream rejects bad chunk (${name}) for ${format}`, async () => { + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + const writePromise = writer.write(value); + const readPromise = reader.read(); + + await assert.rejects(writePromise, expected); + await assert.rejects(readPromise, expected); + }); + } +} + +// Verify that decompression errors (e.g. corrupt data) are surfaced as +// TypeError, not plain Error, per the Compression Streams spec. +for (const format of ['deflate', 'deflate-raw', 'gzip', 'brotli']) { + test(`DecompressionStream surfaces corrupt data as TypeError for ${format}`, async () => { + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + const corruptData = new Uint8Array([0, 1, 2, 3, 4, 5]); + + writer.write(corruptData).catch(() => {}); + reader.read().catch(() => {}); + + await assert.rejects(writer.close(), { name: 'TypeError' }); + await assert.rejects(reader.closed, { name: 'TypeError' }); + }); +} diff --git a/test/parallel/test-webstreams-decompression-reject-trailing.js b/test/parallel/test-webstreams-decompression-reject-trailing.js new file mode 100644 index 00000000000000..3efc97578e42f7 --- /dev/null +++ b/test/parallel/test-webstreams-decompression-reject-trailing.js @@ -0,0 +1,86 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const test = require('node:test'); +const { CompressionStream, DecompressionStream } = require('stream/web'); + +// Verify that DecompressionStream rejects trailing data after a valid +// compressed payload for all four supported formats (deflate, deflate-raw, gzip, brotli). + +const input = Buffer.from('hello'); +const trailingJunk = Buffer.from([0xDE, 0xAD]); + +async function compress(format, data) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(data); + writer.close(); + const chunks = await Array.fromAsync(cs.readable); + return Buffer.concat(chunks.map((c) => Buffer.from(c))); +} + +for (const format of ['deflate', 'deflate-raw', 'gzip', 'brotli']) { + test(`DecompressionStream rejects trailing junk for ${format}`, async () => { + const compressed = await compress(format, input); + const withJunk = Buffer.concat([compressed, trailingJunk]); + + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + writer.write(withJunk).catch(() => {}); + writer.close().catch(() => {}); + + await assert.rejects(async () => { + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + }, (err) => { + assert(err instanceof Error, `Expected Error, got ${err?.constructor?.name}`); + return true; + }); + }); + + test(`DecompressionStream accepts valid ${format} data without trailing junk`, async () => { + const compressed = await compress(format, input); + + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + + writer.write(compressed); + writer.close(); + + const chunks = await Array.fromAsync(ds.readable); + const result = Buffer.concat(chunks.map((c) => Buffer.from(c))); + assert.strictEqual(result.toString(), 'hello'); + }); +} + +// Extra: Verify that trailing data is also rejected when passed as a separate +// chunk after the valid compressed data has been fully written. +for (const format of ['deflate', 'deflate-raw', 'gzip', 'brotli']) { + test(`DecompressionStream rejects trailing junk as separate chunk for ${format}`, async () => { + const compressed = await compress(format, input); + + const ds = new DecompressionStream(format); + const writer = ds.writable.getWriter(); + const reader = ds.readable.getReader(); + + writer.write(compressed).catch(() => {}); + writer.write(trailingJunk).catch(() => {}); + writer.close().catch(() => {}); + + await assert.rejects(async () => { + while (true) { + const { done } = await reader.read(); + if (done) break; + } + }, (err) => { + assert(err instanceof Error, `Expected Error, got ${err?.constructor?.name}`); + return true; + }); + }); +} diff --git a/test/wpt/status/compression.json b/test/wpt/status/compression.json index 619add6fbc25a9..5c73f02a587176 100644 --- a/test/wpt/status/compression.json +++ b/test/wpt/status/compression.json @@ -1,11 +1,5 @@ { - "compression-bad-chunks.tentative.any.js": { - "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" - }, - "decompression-bad-chunks.tentative.any.js": { - "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" - }, - "compression-with-detach.tentative.window.js": { + "compression-with-detach.window.js": { "requires": ["crypto"] }, "idlharness-shadowrealm.window.js": {