| // 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 |
| // number of tests, we only mutate one field at a time, and we only test |
| // "interesting" values. |
| |
| 'use strict'; |
| |
| // The many different cases are summarised in this data structure. |
| const expectations = [ |
| { |
| format: 'deflate', |
| |
| // Decompresses to 'expected output'. |
| baseInput: deflateChunkValue, |
| |
| // See RFC1950 for the definition of the various fields used by deflate: |
| // https://tools.ietf.org/html/rfc1950. |
| fields: [ |
| { |
| // The function of this field. This matches the name used in the RFC. |
| name: 'CMF', |
| |
| // The offset of the field in bytes from the start of the input. |
| offset: 0, |
| |
| // The length of the field in bytes. |
| length: 1, |
| |
| cases: [ |
| { |
| // The value to set the field to. If the field contains multiple |
| // bytes, all the bytes will be set to this value. |
| value: 0, |
| |
| // The expected result. 'success' means the input is decoded |
| // successfully. 'error' means that the stream will be errored. |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'FLG', |
| offset: 1, |
| length: 1, |
| |
| // FLG contains a 4-bit checksum (FCHECK) which is calculated in such a |
| // way that there are 4 valid values for this field. |
| cases: [ |
| { |
| value: 218, |
| result: 'success' |
| }, |
| { |
| value: 1, |
| result: 'success' |
| }, |
| { |
| value: 94, |
| result: 'success' |
| }, |
| { |
| // The remaining 252 values cause an error. |
| value: 157, |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'DATA', |
| // In general, changing any bit of the data will trigger a checksum |
| // error. Only the last byte does anything else. |
| offset: 18, |
| length: 1, |
| cases: [ |
| { |
| value: 4, |
| result: 'success' |
| }, |
| { |
| value: 5, |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'ADLER', |
| offset: -4, |
| length: 4, |
| cases: [ |
| { |
| value: 255, |
| result: 'error' |
| } |
| ] |
| } |
| ] |
| }, |
| { |
| format: 'gzip', |
| |
| // Decompresses to 'expected output'. |
| baseInput: gzipChunkValue, |
| |
| // See RFC1952 for the definition of the various fields used by gzip: |
| // https://tools.ietf.org/html/rfc1952. |
| fields: [ |
| { |
| name: 'ID', |
| offset: 0, |
| length: 2, |
| cases: [ |
| { |
| value: 255, |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'CM', |
| offset: 2, |
| length: 1, |
| cases: [ |
| { |
| value: 0, |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'FLG', |
| offset: 3, |
| length: 1, |
| cases: [ |
| { |
| value: 1, // FTEXT |
| result: 'success' |
| }, |
| { |
| value: 2, // FHCRC |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'MTIME', |
| offset: 4, |
| length: 4, |
| cases: [ |
| { |
| // Any value is valid for this field. |
| value: 255, |
| result: 'success' |
| } |
| ] |
| }, |
| { |
| name: 'XFL', |
| offset: 8, |
| length: 1, |
| cases: [ |
| { |
| // Any value is accepted. |
| value: 255, |
| result: 'success' |
| } |
| ] |
| }, |
| { |
| name: 'OS', |
| offset: 9, |
| length: 1, |
| cases: [ |
| { |
| // Any value is accepted. |
| value: 128, |
| result: 'success' |
| } |
| ] |
| }, |
| { |
| name: 'DATA', |
| |
| // The last byte of the data is the most interesting. |
| offset: 26, |
| length: 1, |
| cases: [ |
| { |
| value: 3, |
| result: 'error' |
| }, |
| { |
| value: 4, |
| result: 'success' |
| } |
| ] |
| }, |
| { |
| name: 'CRC', |
| offset: -8, |
| length: 4, |
| cases: [ |
| { |
| // Any change will error the stream. |
| value: 0, |
| result: 'error' |
| } |
| ] |
| }, |
| { |
| name: 'ISIZE', |
| offset: -4, |
| length: 4, |
| cases: [ |
| { |
| // A mismatch will error the stream. |
| value: 1, |
| result: 'error' |
| } |
| ] |
| } |
| ] |
| } |
| ]; |
| |
| async function tryDecompress(input, format) { |
| const ds = new DecompressionStream(format); |
| const reader = ds.readable.getReader(); |
| const writer = ds.writable.getWriter(); |
| writer.write(input).catch(() => {}); |
| writer.close().catch(() => {}); |
| let out = []; |
| while (true) { |
| try { |
| const { value, done } = await reader.read(); |
| if (done) { |
| break; |
| } |
| out = out.concat(Array.from(value)); |
| } catch (e) { |
| if (e instanceof TypeError) { |
| return { result: 'error' }; |
| } else { |
| return { result: e.name }; |
| } |
| } |
| } |
| const expectedOutput = 'expected output'; |
| if (out.length !== expectedOutput.length) { |
| return { result: 'corrupt' }; |
| } |
| for (let i = 0; i < out.length; ++i) { |
| if (out[i] !== expectedOutput.charCodeAt(i)) { |
| return { result: 'corrupt' }; |
| } |
| } |
| return { result: 'success' }; |
| } |
| |
| function corruptInput(input, offset, length, value) { |
| const output = new Uint8Array(input); |
| if (offset < 0) { |
| offset += input.length; |
| } |
| for (let i = offset; i < offset + length; ++i) { |
| output[i] = value; |
| } |
| return output; |
| } |
| |
| for (const { format, baseInput, fields } of expectations) { |
| promise_test(async () => { |
| 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 = 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, 0]); |
| const { result } = await tryDecompress(extendedInput, format); |
| assert_equals(result, 'error', 'decompression should fail'); |
| }, `trailing junk for '${format}' should give an error`); |
| |
| for (const { name, offset, length, cases } of fields) { |
| for (const { value, result } of cases) { |
| promise_test(async () => { |
| const corruptedInput = corruptInput(baseInput, offset, length, value); |
| const { result: actual } = |
| await tryDecompress(corruptedInput, format); |
| assert_equals(actual, result, 'result should match'); |
| }, `format '${format}' field ${name} should be ${result} for ${value}`); |
| } |
| } |
| } |
| |
| promise_test(async () => { |
| // Data generated in Python: |
| // ```py |
| // h = b"thequickbrownfoxjumped\x00" |
| // words = h.split() |
| // zdict = b''.join(words) |
| // co = zlib.compressobj(zdict=zdict) |
| // cd = co.compress(h) + co.flush() |
| // ``` |
| const { result } = await tryDecompress(new Uint8Array([ |
| 0x78, 0xbb, 0x74, 0xee, 0x09, 0x59, 0x2b, 0xc1, 0x2e, 0x0c, 0x00, 0x74, 0xee, 0x09, 0x59 |
| ]), 'deflate'); |
| assert_equals(result, 'error', 'Data compressed with a dictionary should throw TypeError'); |
| }, 'the deflate input compressed with dictionary should give an error') |