/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Tests DER.jsm functionality.

// Until DER.jsm is actually used in production code, this is where we have to
// import it from.
var { DER } = ChromeUtils.importESModule(
  "resource://gre/modules/psm/DER.sys.mjs"
);

function run_simple_tests() {
  throws(
    () => new DER.DERDecoder("this is not an array"),
    /invalid input/,
    "should throw given non-array input"
  );

  let testReadByte = new DER.DERDecoder([0x0a, 0x0b]);
  equal(testReadByte.readByte(), 0x0a, "should read 0x0a");
  equal(testReadByte.readByte(), 0x0b, "should read 0x0b");
  throws(
    () => testReadByte.readByte(),
    /data truncated/,
    "reading more data than is available should fail"
  );

  let testReadBytes = new DER.DERDecoder([0x0c, 0x0d, 0x0e]);
  deepEqual(
    testReadBytes.readBytes(3),
    [0x0c, 0x0d, 0x0e],
    "should read correct sequence of bytes"
  );

  let testReadNegativeBytes = new DER.DERDecoder([0xff, 0xaf]);
  throws(
    () => testReadNegativeBytes.readBytes(-4),
    /invalid length/,
    "reading a negative number of bytes should fail"
  );

  let testReadZeroBytes = new DER.DERDecoder([]);
  equal(
    testReadZeroBytes.readBytes(0).length,
    0,
    "reading zero bytes should result in a zero-length array"
  );

  let testReadTooManyBytes = new DER.DERDecoder([0xab, 0xcd, 0xef]);
  throws(
    () => testReadTooManyBytes.readBytes(4),
    /data truncated/,
    "reading too many bytes should fail"
  );

  let testSEQUENCE = new DER.DERDecoder([0x30, 0x01, 0x01]);
  let content = testSEQUENCE.readTagAndGetContents(DER.SEQUENCE);
  equal(content.length, 1, "content should have length 1");
  equal(content[0], 1, "value of content should be [1]");
  ok(testSEQUENCE.atEnd(), "testSEQUENCE should be at the end of its input");
  testSEQUENCE.assertAtEnd();

  // The length purports to be 4 bytes, but there are only 2 available.
  let truncatedSEQUENCE = new DER.DERDecoder([0x30, 0x04, 0x00, 0x00]);
  throws(
    () => truncatedSEQUENCE.readTagAndGetContents(DER.SEQUENCE),
    /data truncated/,
    "should get 'data truncated' error"
  );

  // With 2 bytes of content, there is 1 remaining after reading the content.
  let extraDataSEQUENCE = new DER.DERDecoder([0x30, 0x02, 0xab, 0xcd, 0xef]);
  content = extraDataSEQUENCE.readTagAndGetContents(DER.SEQUENCE);
  equal(content.length, 2, "content should have length 2");
  deepEqual(content, [0xab, 0xcd], "value of content should be [0xab, 0xcd]");
  ok(
    !extraDataSEQUENCE.atEnd(),
    "extraDataSEQUENCE should not be at the end of its input"
  );
  throws(
    () => extraDataSEQUENCE.assertAtEnd(),
    /extra data/,
    "should get 'extra data' error"
  );

  // The length of 0x81 0x01 is invalid because it could be encoded as just
  // 0x01, which is shorter.
  let invalidLengthSEQUENCE1 = new DER.DERDecoder([0x30, 0x81, 0x01, 0x00]);
  throws(
    () => invalidLengthSEQUENCE1.readTagAndGetContents(DER.SEQUENCE),
    /invalid length/,
    "should get 'invalid length' error"
  );

  // Similarly, 0x82 0x00 0x01 could be encoded as just 0x01, which is shorter.
  let invalidLengthSEQUENCE2 = new DER.DERDecoder([
    0x30, 0x82, 0x00, 0x01, 0x00,
  ]);
  throws(
    () => invalidLengthSEQUENCE2.readTagAndGetContents(DER.SEQUENCE),
    /invalid length/,
    "should get 'invalid length' error"
  );

  // Lengths requiring 4 bytes to encode are not supported.
  let unsupportedLengthSEQUENCE = new DER.DERDecoder([
    0x30, 0x83, 0x01, 0x01, 0x01,
  ]);
  throws(
    () => unsupportedLengthSEQUENCE.readTagAndGetContents(DER.SEQUENCE),
    /unsupported length/,
    "should get 'unsupported length' error"
  );

  // Indefinite lengths are not supported (and aren't DER anyway).
  let unsupportedASN1SEQUENCE = new DER.DERDecoder([
    0x30, 0x80, 0x01, 0x00, 0x00,
  ]);
  throws(
    () => unsupportedASN1SEQUENCE.readTagAndGetContents(DER.SEQUENCE),
    /unsupported asn.1/,
    "should get 'unsupported asn.1' error"
  );

  let unexpectedTag = new DER.DERDecoder([0x31, 0x01, 0x00]);
  throws(
    () => unexpectedTag.readTagAndGetContents(DER.SEQUENCE),
    /unexpected tag/,
    "should get 'unexpected tag' error"
  );

  let readTLVTestcase = new DER.DERDecoder([0x02, 0x03, 0x45, 0x67, 0x89]);
  let bytes = readTLVTestcase.readTLV();
  deepEqual(
    bytes,
    [0x02, 0x03, 0x45, 0x67, 0x89],
    "bytes read with readTLV should be equal to expected value"
  );

  let peekTagTestcase = new DER.DERDecoder([0x30, 0x01, 0x00]);
  ok(
    peekTagTestcase.peekTag(DER.SEQUENCE),
    "peekTag should return true for peeking with a SEQUENCE at a SEQUENCE"
  );
  ok(
    !peekTagTestcase.peekTag(DER.SET),
    "peekTag should return false for peeking with a SET at a SEQUENCE"
  );
  peekTagTestcase.readTLV();
  ok(
    !peekTagTestcase.peekTag(DER.SEQUENCE),
    "peekTag should return false for peeking at a DER with no more data"
  );

  let tlvChoiceTestcase = new DER.DERDecoder([0x31, 0x02, 0xaa, 0xbb]);
  let tlvChoiceContents = tlvChoiceTestcase.readTLVChoice([DER.NULL, DER.SET]);
  deepEqual(
    tlvChoiceContents,
    [0x31, 0x02, 0xaa, 0xbb],
    "readTLVChoice should return expected bytes"
  );

  let tlvChoiceNoMatchTestcase = new DER.DERDecoder([0x30, 0x01, 0xff]);
  throws(
    () => tlvChoiceNoMatchTestcase.readTLVChoice([DER.NULL, DER.SET]),
    /unexpected tag/,
    "readTLVChoice should throw if no matching tag is found"
  );
}

function run_bit_string_tests() {
  let bitstringDER = new DER.DERDecoder([0x03, 0x04, 0x03, 0x01, 0x02, 0xf8]);
  let bitstring = bitstringDER.readBIT_STRING();
  equal(bitstring.unusedBits, 3, "BIT STRING should have 3 unused bits");
  deepEqual(
    bitstring.contents,
    [0x01, 0x02, 0xf8],
    "BIT STRING should have expected contents"
  );

  let bitstringTooManyUnusedBits = new DER.DERDecoder([0x03, 0x02, 0x08, 0x00]);
  throws(
    () => bitstringTooManyUnusedBits.readBIT_STRING(),
    /invalid BIT STRING encoding/,
    "BIT STRING with too many unused bits should throw"
  );

  // A BIT STRING must have the unused bits byte, and so its length must be at
  // least one.
  let bitstringMissingUnusedBits = new DER.DERDecoder([0x03, 0x00]);
  throws(
    () => bitstringMissingUnusedBits.readBIT_STRING(),
    /invalid BIT STRING encoding/,
    "BIT STRING with missing unused bits (and no contents) should throw"
  );

  // The minimal BIT STRING is 03 01 00 (zero bits of padding and zero bytes of
  // content).
  let minimalBitstringDER = new DER.DERDecoder([0x03, 0x01, 0x00]);
  let minimalBitstring = minimalBitstringDER.readBIT_STRING();
  equal(
    minimalBitstring.unusedBits,
    0,
    "minimal BIT STRING should have 0 unused bits"
  );
  equal(
    minimalBitstring.contents.length,
    0,
    "minimal BIT STRING should have empty contents"
  );

  // However, a BIT STRING with zero bytes of content can't have any padding,
  // because that makes no sense.
  let noContentsPaddedBitstringDER = new DER.DERDecoder([0x03, 0x01, 0x03]);
  throws(
    () => noContentsPaddedBitstringDER.readBIT_STRING(),
    /invalid BIT STRING encoding/,
    "BIT STRING with no contents with non-zero padding should throw"
  );
}

function run_compound_tests() {
  let derBytes = [
    0x30,
    0x1a, // SEQUENCE
    0x02,
    0x02,
    0x77,
    0xff, //   INTEGER
    0x06,
    0x03,
    0x2b,
    0x01,
    0x01, //   OBJECT IDENTIFIER
    0x30,
    0x07, //   SEQUENCE
    0x05,
    0x00, //     NULL
    0x02,
    0x03,
    0x45,
    0x46,
    0x47, //     INTEGER
    0x30,
    0x06, //   SEQUENCE
    0x02,
    0x02,
    0x00,
    0xff, //     INTEGER
    0x05,
    0x00,
  ]; //     NULL
  let der = new DER.DERDecoder(derBytes);
  let contents = new DER.DERDecoder(der.readTagAndGetContents(DER.SEQUENCE));
  let firstINTEGER = contents.readTagAndGetContents(DER.INTEGER);
  deepEqual(
    firstINTEGER,
    [0x77, 0xff],
    "first INTEGER should have expected value"
  );
  let oid = contents.readTagAndGetContents(DER.OBJECT_IDENTIFIER);
  deepEqual(
    oid,
    [0x2b, 0x01, 0x01],
    "OBJECT IDENTIFIER should have expected value"
  );

  let firstNested = new DER.DERDecoder(
    contents.readTagAndGetContents(DER.SEQUENCE)
  );
  let firstNestedNULL = firstNested.readTagAndGetContents(DER.NULL);
  equal(
    firstNestedNULL.length,
    0,
    "first nested NULL should have expected value (empty array)"
  );
  let firstNestedINTEGER = firstNested.readTagAndGetContents(DER.INTEGER);
  deepEqual(
    firstNestedINTEGER,
    [0x45, 0x46, 0x47],
    "first nested INTEGER should have expected value"
  );
  firstNested.assertAtEnd();

  let secondNested = new DER.DERDecoder(
    contents.readTagAndGetContents(DER.SEQUENCE)
  );
  let secondNestedINTEGER = secondNested.readTagAndGetContents(DER.INTEGER);
  deepEqual(
    secondNestedINTEGER,
    [0x00, 0xff],
    "second nested INTEGER should have expected value"
  );
  let secondNestedNULL = secondNested.readTagAndGetContents(DER.NULL);
  equal(
    secondNestedNULL.length,
    0,
    "second nested NULL should have expected value (empty array)"
  );
  secondNested.assertAtEnd();

  contents.assertAtEnd();
  der.assertAtEnd();

  let invalidDERBytes = [
    0x30,
    0x06, // SEQUENCE
    0x30,
    0x02, //   SEQUENCE
    0x02,
    0x01, //     INTEGER (missing data)
    0x05,
    0x00, //   NULL
    0x00,
    0x00,
  ]; // (extra data)
  let invalidDER = new DER.DERDecoder(invalidDERBytes);
  let invalidContents = new DER.DERDecoder(
    invalidDER.readTagAndGetContents(DER.SEQUENCE)
  );
  let invalidContentsContents = new DER.DERDecoder(
    invalidContents.readTagAndGetContents(DER.SEQUENCE)
  );
  throws(
    () => invalidContentsContents.readTagAndGetContents(DER.INTEGER),
    /data truncated/,
    "should throw due to missing data"
  );
  let nestedNULL = invalidContents.readTagAndGetContents(DER.NULL);
  equal(nestedNULL.length, 0, "nested NULL should have expected value");
  invalidContents.assertAtEnd();
  throws(
    () => invalidDER.assertAtEnd(),
    /extra data/,
    "should throw due to extra data"
  );
}

function run_test() {
  run_simple_tests();
  run_bit_string_tests();
  run_compound_tests();
}
