forked from mirrors/gecko-dev
318 lines
7.9 KiB
JavaScript
318 lines
7.9 KiB
JavaScript
// META global=window,worker
|
|
|
|
// 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: [120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41,
|
|
40, 45, 1, 0, 48, 173, 6, 36],
|
|
|
|
// 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: [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],
|
|
|
|
// 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(new Uint8Array(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 { 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 { 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')
|