// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';
class Cbor {
  constructor(buffer) {
    this.slice = new Uint8Array(buffer);
  }
  get data() {
    return this.slice;
  }
  get length() {
    return this.slice.length;
  }
  get empty() {
    return this.slice.length == 0;
  }
  get hex() {
    const hexTable = '0123456789abcdef';
    let s = '';
    for (let i = 0; i < this.data.length; i++) {
      s += hexTable.charAt(this.data[i] >> 4);
      s += hexTable.charAt(this.data[i] & 15);
    }
    return s;
  }
  base64Encode(chars, padding) {
    const len3 = 3 * Math.floor(this.slice.length / 3);
    var chunks = [];
    for (let i = 0; i < len3; i += 3) {
      const v =
          (this.slice[i] << 16) + (this.slice[i + 1] << 8) + this.slice[i + 2];
      chunks.push(
          chars[v >> 18] + chars[(v >> 12) & 0x3f] + chars[(v >> 6) & 0x3f] +
          chars[v & 0x3f]);
    }
    const remainder = this.slice.length - len3;
    if (remainder == 1) {
      const v = this.slice[len3];
      chunks.push(chars[v >> 2] + chars[(v << 4) & 0x3f]);
      if (padding == 1 /* Include */) {
        chunks.push('==');
      }
    } else if (remainder == 2) {
      const v = (this.slice[len3] << 8) + this.slice[len3 + 1];
      chunks.push(
          chars[v >> 10] + chars[(v >> 4) & 0x3f] + chars[(v << 2) & 0x3f]);
      if (padding == 1 /* Include */) {
        chunks.push('=');
      }
    }
    return chunks.join('');
  }
  webSafeBase64() {
    const chars =
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
    return this.base64Encode(chars, 0 /* None */);
  }
  base64() {
    const chars =
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    return this.base64Encode(chars, 1 /* Include */);
  }
  compare(other) {
    if (this.length < other.length) {
      return -1;
    } else if (this.length > other.length) {
      return 1;
    }
    for (let i = 0; i < this.length; i++) {
      if (this.slice[i] < other.slice[i]) {
        return -1;
      } else if (this.slice[i] > other.slice[i]) {
        return 1;
      }
    }
    return 0;
  }
  getU8() {
    if (this.empty) {
      throw('Cbor: empty during getU8');
    }
    const byte = this.slice[0];
    this.slice = this.slice.subarray(1);
    return byte;
  }
  skip(n) {
    if (this.length < n) {
      throw('Cbor: too few bytes to skip');
    }
    this.slice = this.slice.subarray(n);
  }
  getBytes(n) {
    if (this.length < n) {
      throw('Cbor: insufficient bytes in getBytes');
    }
    const ret = this.slice.subarray(0, n);
    this.slice = this.slice.subarray(n);
    return ret;
  }
  getUnsigned(n) {
    const bytes = this.getBytes(n);
    let value = 0;
    for (let i = 0; i < n; i++) {
      value <<= 8;
      value |= bytes[i];
    }
    return value;
  }
  getU16() {
    return this.getUnsigned(2);
  }
  getU32() {
    return this.getUnsigned(4);
  }
  getASN1_(expectedTag, includeHeader) {
    if (this.empty) {
      throw 'getASN1: empty slice, expected tag ' + expectedTag;
    }
    const v = this.getAnyASN1();
    if (v.tag != expectedTag) {
      throw 'getASN1: got tag ' + v.tag + ', want ' + expectedTag;
    }
    if (!includeHeader) {
      v.val.skip(v.headerLen);
    }
    return v.val;
  }
  getASN1(expectedTag) {
    return this.getASN1_(expectedTag, false);
  }
  getASN1Element(expectedTag) {
    return this.getASN1_(expectedTag, true);
  }
  getOptionalASN1(expectedTag) {
    if (this.slice.length < 1 || this.slice[0] != expectedTag) {
      return null;
    }
    return this.getASN1(expectedTag);
  }
  getAnyASN1() {
    const header = new Cbor(this.slice);
    const tag = header.getU8();
    const lengthByte = header.getU8();
    if ((tag & 0x1f) == 0x1f) {
      throw 'getAnyASN1: long-form tag found';
    }
    let len = 0;
    let headerLen = 0;
    if ((lengthByte & 0x80) == 0) {
      // Short form length.
      len = lengthByte + 2;
      headerLen = 2;
    } else {
      // The high bit indicates that this is the long form, while the next 7
      // bits encode the number of subsequent octets used to encode the length
      // (ITU-T X.690 clause 8.1.3.5.b).
      const numBytes = lengthByte & 0x7f;
      // Bitwise operations are always on signed 32-bit two's complement
      // numbers.  This check ensures that we stay under this limit.  We could
      // do this in a better way, but there's no need to process very large
      // objects.
      if (numBytes == 0 || numBytes > 3) {
        throw 'getAnyASN1: bad ASN.1 long-form length';
      }
      const lengthBytes = header.getBytes(numBytes);
      for (let i = 0; i < numBytes; i++) {
        len <<= 8;
        len |= lengthBytes[i];
      }
      if (len < 128 || (len >> ((numBytes - 1) * 8)) == 0) {
        throw 'getAnyASN1: incorrectly encoded ASN.1 length';
      }
      headerLen = 2 + numBytes;
      len += headerLen;
    }
    if (this.slice.length < len) {
      throw 'getAnyASN1: too few bytes in input';
    }
    const prefix = this.slice.subarray(0, len);
    this.slice = this.slice.subarray(len);
    return {tag: tag, headerLen: headerLen, val: new Cbor(prefix)};
  }
  getBase128Int() {
    let lookahead = this.slice.length;
    if (lookahead > 4) {
      lookahead = 4;
    }
    let len = 0;
    for (let i = 0; i < lookahead; i++) {
      if (!(this.slice[i] & 0x80)) {
        len = i + 1;
        break;
      }
    }
    if (len == 0) {
      throw 'base128 value too large';
    }
    let n = 0;
    let octets = this.getBytes(len);
    for (let i = 0; i < len; i++) {
      if ((n & 0xff000000) != 0) {
        throw 'base128 value too large';
      }
      n <<= 7;
      n |= octets[i] & 0x7f;
    }
    return n;
  }
  getASN1ObjectIdentifier() {
    let b = this.getASN1(6 /* OBJECT */);
    let first = b.getBase128Int();
    let result = [0, 0];
    result[1] = first % 40;
    result[0] = (first - result[1]) / 40;
    while (!b.empty) {
      result.push(b.getBase128Int());
    }
    return result;
  }
  getCBORHeader() {
    const copy = new Cbor(this.slice);
    const a = this.getU8();
    const majorType = a >> 5;
    const info = a & 31;
    if (info < 24) {
      return [majorType, info, new Cbor(copy.getBytes(1))];
    } else if (info < 28) {
      const lengthLength = 1 << (info - 24);
      let data = this.getBytes(lengthLength);
      let value = 0;
      for (let i = 0; i < lengthLength; i++) {
        // Javascript has problems handling uint64s given the limited range of
        // a double.
        if (value > 35184372088831) {
          throw('Cbor: cannot represent CBOR number');
        }
        // Not using bitwise operations to avoid truncating to 32 bits.
        value *= 256;
        value += data[i];
      }
      switch (lengthLength) {
        case 1:
          if (value < 24) {
            throw('Cbor: value should have been encoded in single byte');
          }
          break;
        case 2:
          if (value < 256) {
            throw('Cbor: non-minimal integer');
          }
          break;
        case 4:
          if (value < 65536) {
            throw('Cbor: non-minimal integer');
          }
          break;
        case 8:
          if (value < 4294967296) {
            throw('Cbor: non-minimal integer');
          }
          break;
      }
      return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))];
    } else {
      throw('Cbor: CBOR contains unhandled info value ' + info);
    }
  }
  getCBOR() {
    const [major, value] = this.getCBORHeader();
    switch (major) {
      case 0:
        return value;
      case 1:
        return 0 - (1 + value);
      case 2:
        return this.getBytes(value);
      case 3:
        return this.getBytes(value);
      case 4: {
        let ret = new Array(value);
        for (let i = 0; i < value; i++) {
          ret[i] = this.getCBOR();
        }
        return ret;
      }
      case 5:
        if (value == 0) {
          return {};
        }
        let copy = new Cbor(this.data);
        const [firstKeyMajor] = copy.getCBORHeader();
        if (firstKeyMajor == 3) {
          // String-keyed map.
          let lastKeyHeader = new Cbor(new Uint8Array(0));
          let lastKeyBytes = new Cbor(new Uint8Array(0));
          let ret = {};
          for (let i = 0; i < value; i++) {
            const [keyMajor, keyLength, keyHeader] = this.getCBORHeader();
            if (keyMajor != 3) {
              throw('Cbor: non-string in string-valued map');
            }
            const keyBytes = new Cbor(this.getBytes(keyLength));
            if (i > 0) {
              const headerCmp = lastKeyHeader.compare(keyHeader);
              if (headerCmp > 0 ||
                  (headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) {
                throw(
                    'Cbor: map keys in wrong order: ' + lastKeyHeader.hex +
                    '/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' +
                    keyBytes.hex);
              }
            }
            lastKeyHeader = keyHeader;
            lastKeyBytes = keyBytes;
            ret[keyBytes.parseUTF8()] = this.getCBOR();
          }
          return ret;
        } else if (firstKeyMajor == 0 || firstKeyMajor == 1) {
          // Number-keyed map.
          let lastKeyHeader = new Cbor(new Uint8Array(0));
          let ret = {};
          for (let i = 0; i < value; i++) {
            let [keyMajor, keyValue, keyHeader] = this.getCBORHeader();
            if (keyMajor != 0 && keyMajor != 1) {
              throw('Cbor: non-number in number-valued map');
            }
            if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) {
              throw(
                  'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' +
                  keyHeader.hex);
            }
            lastKeyHeader = keyHeader;
            if (keyMajor == 1) {
              keyValue = 0 - (1 + keyValue);
            }
            ret[keyValue] = this.getCBOR();
          }
          return ret;
        } else {
          throw('Cbor: map keyed by invalid major type ' + firstKeyMajor);
        }
      default:
        throw('Cbor: unhandled major type ' + major);
    }
  }
  parseUTF8() {
    return (new TextDecoder('utf-8')).decode(this.slice);
  }
}
