From 9247c562b36f6a8f0b68f984089bdc40f88069ae Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 4 Mar 2026 22:32:02 +0100 Subject: [PATCH 1/4] test: update WPT compression to ae05f5cb53 --- test/fixtures/wpt/README.md | 2 +- .../compression/compression-bad-chunks.any.js | 57 +++++ .../compression-bad-chunks.tentative.any.js | 74 ------- ...s => compression-constructor-error.any.js} | 0 ... compression-including-empty-chunk.any.js} | 29 +-- .../compression-large-flush-output.any.js | 28 +-- .../compression-multiple-chunks.any.js | 58 +++++ ...mpression-multiple-chunks.tentative.any.js | 67 ------ .../compression-output-length.any.js | 47 ++++ ...compression-output-length.tentative.any.js | 64 ------ .../wpt/compression/compression-stream.any.js | 59 +++++ .../compression-stream.tentative.any.js | 91 -------- ...w.js => compression-with-detach.window.js} | 0 ...any.js => decompression-bad-chunks.any.js} | 19 +- .../decompression-buffersource.any.js | 86 ++++++++ ...ecompression-buffersource.tentative.any.js | 204 ------------------ ...=> decompression-constructor-error.any.js} | 0 .../decompression-correct-input.any.js | 15 ++ ...compression-correct-input.tentative.any.js | 39 ---- ....js => decompression-corrupt-input.any.js} | 24 ++- .../decompression-empty-input.any.js | 24 +++ ...decompression-empty-input.tentative.any.js | 43 ---- .../decompression-extra-input.any.js | 20 ++ .../decompression-split-chunk.any.js | 38 ++++ ...decompression-split-chunk.tentative.any.js | 53 ----- .../decompression-uint8array-output.any.js | 16 ++ ...ression-uint8array-output.tentative.any.js | 30 --- ...js => decompression-with-detach.window.js} | 0 .../wpt/compression/resources/decompress.js | 31 +++ .../resources/decompression-input.js | 20 ++ .../wpt/compression/resources/formats.js | 6 + test/fixtures/wpt/versions.json | 2 +- test/wpt/status/compression.json | 6 +- 33 files changed, 525 insertions(+), 727 deletions(-) create mode 100644 test/fixtures/wpt/compression/compression-bad-chunks.any.js delete mode 100644 test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js rename test/fixtures/wpt/compression/{compression-constructor-error.tentative.any.js => compression-constructor-error.any.js} (100%) rename test/fixtures/wpt/compression/{compression-including-empty-chunk.tentative.any.js => compression-including-empty-chunk.any.js} (52%) create mode 100644 test/fixtures/wpt/compression/compression-multiple-chunks.any.js delete mode 100644 test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-output-length.any.js delete mode 100644 test/fixtures/wpt/compression/compression-output-length.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-stream.any.js delete mode 100644 test/fixtures/wpt/compression/compression-stream.tentative.any.js rename test/fixtures/wpt/compression/{compression-with-detach.tentative.window.js => compression-with-detach.window.js} (100%) rename test/fixtures/wpt/compression/{decompression-bad-chunks.tentative.any.js => decompression-bad-chunks.any.js} (82%) create mode 100644 test/fixtures/wpt/compression/decompression-buffersource.any.js delete mode 100644 test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js rename test/fixtures/wpt/compression/{decompression-constructor-error.tentative.any.js => decompression-constructor-error.any.js} (100%) create mode 100644 test/fixtures/wpt/compression/decompression-correct-input.any.js delete mode 100644 test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js rename test/fixtures/wpt/compression/{decompression-corrupt-input.tentative.any.js => decompression-corrupt-input.any.js} (93%) create mode 100644 test/fixtures/wpt/compression/decompression-empty-input.any.js delete mode 100644 test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-extra-input.any.js create mode 100644 test/fixtures/wpt/compression/decompression-split-chunk.any.js delete mode 100644 test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-uint8array-output.any.js delete mode 100644 test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js rename test/fixtures/wpt/compression/{decompression-with-detach.tentative.window.js => decompression-with-detach.window.js} (100%) create mode 100644 test/fixtures/wpt/compression/resources/decompress.js create mode 100644 test/fixtures/wpt/compression/resources/decompression-input.js create mode 100644 test/fixtures/wpt/compression/resources/formats.js 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/wpt/status/compression.json b/test/wpt/status/compression.json index 619add6fbc25a9..f9b270b71fc2c2 100644 --- a/test/wpt/status/compression.json +++ b/test/wpt/status/compression.json @@ -1,11 +1,11 @@ { - "compression-bad-chunks.tentative.any.js": { + "compression-bad-chunks.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": { + "decompression-bad-chunks.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": { From 41d5b4142232fd5476550a62b450e4269f0d8bbf Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 4 Mar 2026 22:32:21 +0100 Subject: [PATCH 2/4] test: improve WPT report runner Add incremental report writing after each spec completes and on worker errors so that reports survive if the process is killed before the exit handler runs. Add bytes() method to readAsFetch() to match the Response API used by newer WPT tests. --- test/common/wpt.js | 8 ++++++++ 1 file changed, 8 insertions(+) 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(); From d6fcd138abb43311b8272f9c1739794d28dcdaa1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 4 Mar 2026 22:36:41 +0100 Subject: [PATCH 3/4] stream: improve Web Compression spec compliance Pass rejectGarbageAfterEnd: true to createInflateRaw() and createBrotliDecompress(), matching the behavior already in place for deflate and gzip. The Compression Streams spec treats any data following a valid compressed payload as an error. When the underlying Node.js stream throws synchronously from write() (e.g. zlib rejects an invalid chunk type), destroy the stream so that the readable side is also errored. Without this, the readable side hangs forever waiting for data that will never arrive. Introduce a kValidateChunk callback option in the webstreams adapter layer. Compression streams use this to validate that chunks are BufferSource instances not backed by SharedArrayBuffer, replacing the previous monkey-patching of the underlying handle's write method. Unskip WPT compression bad-chunks tests which now run instead of hang and mark the remaining expected failures. --- lib/internal/webstreams/adapters.js | 44 ++++++++-- lib/internal/webstreams/compression.js | 49 ++++++++--- ...st-webstreams-adapters-sync-write-error.js | 79 +++++++++++++++++ .../test-webstreams-compression-bad-chunks.js | 57 ++++++++++++ ...ebstreams-decompression-reject-trailing.js | 86 +++++++++++++++++++ test/wpt/status/compression.json | 10 ++- 6 files changed, 302 insertions(+), 23 deletions(-) create mode 100644 test/parallel/test-webstreams-adapters-sync-write-error.js create mode 100644 test/parallel/test-webstreams-compression-bad-chunks.js create mode 100644 test/parallel/test-webstreams-decompression-reject-trailing.js diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 83265227a917a9..e4695a83dc4247 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( @@ -139,9 +144,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 +226,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 +682,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 +1090,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/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..442eb8b2ac194b --- /dev/null +++ b/test/parallel/test-webstreams-compression-bad-chunks.js @@ -0,0 +1,57 @@ +'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); + }); + } +} 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 f9b270b71fc2c2..b5c5877db48b55 100644 --- a/test/wpt/status/compression.json +++ b/test/wpt/status/compression.json @@ -1,9 +1,11 @@ { - "compression-bad-chunks.any.js": { - "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" - }, "decompression-bad-chunks.any.js": { - "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" + "fail": { + "expected": [ + "chunk of type invalid deflate bytes should error the stream for brotli", + "chunk of type invalid gzip bytes should error the stream for brotli" + ] + } }, "compression-with-detach.window.js": { "requires": ["crypto"] From c28a421196019be8e1f4d617fbdcef24e2402e70 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 4 Mar 2026 23:37:06 +0100 Subject: [PATCH 4/4] stream: fix brotli error handling in web compression streams Convert brotli decompression errors to TypeError to match the Compression Streams spec, by extending handleKnownInternalErrors() in the adapters layer to recognize brotli error codes. This replaces the manual error event handler on DecompressionStream which was redundant with the adapter's built-in error propagation. --- lib/internal/webstreams/adapters.js | 9 ++++++++- .../test-webstreams-compression-bad-chunks.js | 18 ++++++++++++++++++ test/wpt/status/compression.json | 8 -------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index e4695a83dc4247..1ffb19b4f59a31 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -120,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; diff --git a/test/parallel/test-webstreams-compression-bad-chunks.js b/test/parallel/test-webstreams-compression-bad-chunks.js index 442eb8b2ac194b..4a8ca3cff8a27f 100644 --- a/test/parallel/test-webstreams-compression-bad-chunks.js +++ b/test/parallel/test-webstreams-compression-bad-chunks.js @@ -55,3 +55,21 @@ for (const format of ['deflate', 'deflate-raw', 'gzip', 'brotli']) { }); } } + +// 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/wpt/status/compression.json b/test/wpt/status/compression.json index b5c5877db48b55..5c73f02a587176 100644 --- a/test/wpt/status/compression.json +++ b/test/wpt/status/compression.json @@ -1,12 +1,4 @@ { - "decompression-bad-chunks.any.js": { - "fail": { - "expected": [ - "chunk of type invalid deflate bytes should error the stream for brotli", - "chunk of type invalid gzip bytes should error the stream for brotli" - ] - } - }, "compression-with-detach.window.js": { "requires": ["crypto"] },