decompression-corrupt-input.any.js (8026B)
1 // META: global=window,worker,shadowrealm 2 // META: script=resources/decompression-input.js 3 4 // This test checks that DecompressionStream behaves according to the standard 5 // when the input is corrupted. To avoid a combinatorial explosion in the 6 // number of tests, we only mutate one field at a time, and we only test 7 // "interesting" values. 8 9 'use strict'; 10 11 // The many different cases are summarised in this data structure. 12 const expectations = [ 13 { 14 format: 'deflate', 15 16 // Decompresses to 'expected output'. 17 baseInput: deflateChunkValue, 18 19 // See RFC1950 for the definition of the various fields used by deflate: 20 // https://tools.ietf.org/html/rfc1950. 21 fields: [ 22 { 23 // The function of this field. This matches the name used in the RFC. 24 name: 'CMF', 25 26 // The offset of the field in bytes from the start of the input. 27 offset: 0, 28 29 // The length of the field in bytes. 30 length: 1, 31 32 cases: [ 33 { 34 // The value to set the field to. If the field contains multiple 35 // bytes, all the bytes will be set to this value. 36 value: 0, 37 38 // The expected result. 'success' means the input is decoded 39 // successfully. 'error' means that the stream will be errored. 40 result: 'error' 41 } 42 ] 43 }, 44 { 45 name: 'FLG', 46 offset: 1, 47 length: 1, 48 49 // FLG contains a 4-bit checksum (FCHECK) which is calculated in such a 50 // way that there are 4 valid values for this field. 51 cases: [ 52 { 53 value: 218, 54 result: 'success' 55 }, 56 { 57 value: 1, 58 result: 'success' 59 }, 60 { 61 value: 94, 62 result: 'success' 63 }, 64 { 65 // The remaining 252 values cause an error. 66 value: 157, 67 result: 'error' 68 } 69 ] 70 }, 71 { 72 name: 'DATA', 73 // In general, changing any bit of the data will trigger a checksum 74 // error. Only the last byte does anything else. 75 offset: 18, 76 length: 1, 77 cases: [ 78 { 79 value: 4, 80 result: 'success' 81 }, 82 { 83 value: 5, 84 result: 'error' 85 } 86 ] 87 }, 88 { 89 name: 'ADLER', 90 offset: -4, 91 length: 4, 92 cases: [ 93 { 94 value: 255, 95 result: 'error' 96 } 97 ] 98 } 99 ] 100 }, 101 { 102 format: 'gzip', 103 104 // Decompresses to 'expected output'. 105 baseInput: gzipChunkValue, 106 107 // See RFC1952 for the definition of the various fields used by gzip: 108 // https://tools.ietf.org/html/rfc1952. 109 fields: [ 110 { 111 name: 'ID', 112 offset: 0, 113 length: 2, 114 cases: [ 115 { 116 value: 255, 117 result: 'error' 118 } 119 ] 120 }, 121 { 122 name: 'CM', 123 offset: 2, 124 length: 1, 125 cases: [ 126 { 127 value: 0, 128 result: 'error' 129 } 130 ] 131 }, 132 { 133 name: 'FLG', 134 offset: 3, 135 length: 1, 136 cases: [ 137 { 138 value: 1, // FTEXT 139 result: 'success' 140 }, 141 { 142 value: 2, // FHCRC 143 result: 'error' 144 } 145 ] 146 }, 147 { 148 name: 'MTIME', 149 offset: 4, 150 length: 4, 151 cases: [ 152 { 153 // Any value is valid for this field. 154 value: 255, 155 result: 'success' 156 } 157 ] 158 }, 159 { 160 name: 'XFL', 161 offset: 8, 162 length: 1, 163 cases: [ 164 { 165 // Any value is accepted. 166 value: 255, 167 result: 'success' 168 } 169 ] 170 }, 171 { 172 name: 'OS', 173 offset: 9, 174 length: 1, 175 cases: [ 176 { 177 // Any value is accepted. 178 value: 128, 179 result: 'success' 180 } 181 ] 182 }, 183 { 184 name: 'DATA', 185 186 // The last byte of the data is the most interesting. 187 offset: 26, 188 length: 1, 189 cases: [ 190 { 191 value: 3, 192 result: 'error' 193 }, 194 { 195 value: 4, 196 result: 'success' 197 } 198 ] 199 }, 200 { 201 name: 'CRC', 202 offset: -8, 203 length: 4, 204 cases: [ 205 { 206 // Any change will error the stream. 207 value: 0, 208 result: 'error' 209 } 210 ] 211 }, 212 { 213 name: 'ISIZE', 214 offset: -4, 215 length: 4, 216 cases: [ 217 { 218 // A mismatch will error the stream. 219 value: 1, 220 result: 'error' 221 } 222 ] 223 } 224 ] 225 }, 226 { 227 format: 'brotli', 228 229 // Decompresses to 'expected output'. 230 baseInput: brotliChunkValue, 231 232 fields: [] 233 } 234 ]; 235 236 async function tryDecompress(input, format) { 237 const ds = new DecompressionStream(format); 238 const reader = ds.readable.getReader(); 239 const writer = ds.writable.getWriter(); 240 writer.write(input).catch(() => {}); 241 writer.close().catch(() => {}); 242 let out = []; 243 while (true) { 244 try { 245 const { value, done } = await reader.read(); 246 if (done) { 247 break; 248 } 249 out = out.concat(Array.from(value)); 250 } catch (e) { 251 if (e instanceof TypeError) { 252 return { result: 'error' }; 253 } else { 254 return { result: e.name }; 255 } 256 } 257 } 258 const expectedOutput = 'expected output'; 259 if (out.length !== expectedOutput.length) { 260 return { result: 'corrupt' }; 261 } 262 for (let i = 0; i < out.length; ++i) { 263 if (out[i] !== expectedOutput.charCodeAt(i)) { 264 return { result: 'corrupt' }; 265 } 266 } 267 return { result: 'success' }; 268 } 269 270 function corruptInput(input, offset, length, value) { 271 const output = new Uint8Array(input); 272 if (offset < 0) { 273 offset += input.length; 274 } 275 for (let i = offset; i < offset + length; ++i) { 276 output[i] = value; 277 } 278 return output; 279 } 280 281 for (const { format, baseInput, fields } of expectations) { 282 promise_test(async () => { 283 const { result } = await tryDecompress(baseInput, format); 284 assert_equals(result, 'success', 'decompression should succeed'); 285 }, `the unchanged input for '${format}' should decompress successfully`); 286 287 promise_test(async () => { 288 const truncatedInput = baseInput.subarray(0, -1); 289 const { result } = await tryDecompress(truncatedInput, format); 290 assert_equals(result, 'error', 'decompression should fail'); 291 }, `truncating the input for '${format}' should give an error`); 292 293 promise_test(async () => { 294 const extendedInput = new Uint8Array([...baseInput, 0]); 295 const { result } = await tryDecompress(extendedInput, format); 296 assert_equals(result, 'error', 'decompression should fail'); 297 }, `trailing junk for '${format}' should give an error`); 298 299 for (const { name, offset, length, cases } of fields) { 300 for (const { value, result } of cases) { 301 promise_test(async () => { 302 const corruptedInput = corruptInput(baseInput, offset, length, value); 303 const { result: actual } = 304 await tryDecompress(corruptedInput, format); 305 assert_equals(actual, result, 'result should match'); 306 }, `format '${format}' field ${name} should be ${result} for ${value}`); 307 } 308 } 309 } 310 311 promise_test(async () => { 312 // Data generated in Python: 313 // ```py 314 // h = b"thequickbrownfoxjumped\x00" 315 // words = h.split() 316 // zdict = b''.join(words) 317 // co = zlib.compressobj(zdict=zdict) 318 // cd = co.compress(h) + co.flush() 319 // ``` 320 const { result } = await tryDecompress(new Uint8Array([ 321 0x78, 0xbb, 0x74, 0xee, 0x09, 0x59, 0x2b, 0xc1, 0x2e, 0x0c, 0x00, 0x74, 0xee, 0x09, 0x59 322 ]), 'deflate'); 323 assert_equals(result, 'error', 'Data compressed with a dictionary should throw TypeError'); 324 }, 'the deflate input compressed with dictionary should give an error')