// 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'
          }
        ]
      }
    ]
  },
  {
    format: 'brotli',

    // Decompresses to 'expected output'.
    baseInput: brotliChunkValue,

    fields: []
  }
];

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')
