// ASN.1 JavaScript decoder
// Copyright (c) 2008-2018 Lapo Luchini <lapo@lapo.it>

// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// 
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

(function (undefined) {
"use strict";

var Int10 = (typeof module !== 'undefined') ? require('./int10.js') : window.Int10,
    oids =/* (typeof module !== 'undefined') ? require('./oids.js') : */window.oids,
    ellipsis = "\u2026",
    reTimeS =     /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/,
    reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;

function stringCut(str, len) {
    if (str.length > len)
        str = str.substring(0, len) + ellipsis;
    return str;
}

function Stream(enc, pos) {
    if (enc instanceof Stream) {
        this.enc = enc.enc;
        this.pos = enc.pos;
    } else {
        // enc should be an array or a binary string
        this.enc = enc;
        this.pos = pos;
    }
}
Stream.prototype.get = function (pos) {
    if (pos === undefined)
        pos = this.pos++;
    if (pos >= this.enc.length)
        throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length;
    return (typeof this.enc == "string") ? this.enc.charCodeAt(pos) : this.enc[pos];
};
Stream.prototype.hexDigits = "0123456789ABCDEF";
Stream.prototype.hexByte = function (b) {
    return this.hexDigits.charAt((b >> 4) & 0xF) + this.hexDigits.charAt(b & 0xF);
};
Stream.prototype.hexDump = function (start, end, raw) {
    var s = "";
    for (var i = start; i < end; ++i) {
        s += this.hexByte(this.get(i));
        if (raw !== true)
            switch (i & 0xF) {
            case 0x7: s += "  "; break;
            case 0xF: s += "\n"; break;
            default:  s += " ";
            }
    }
    return s;
};
var b64Safe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Stream.prototype.b64Dump = function (start, end) {
    var extra = (end - start) % 3,
        s = '',
        i, c;
    for (i = start; i + 2 < end; i += 3) {
        c = this.get(i) << 16 | this.get(i + 1) << 8 | this.get(i + 2);
        s += b64Safe.charAt(c >> 18 & 0x3F);
        s += b64Safe.charAt(c >> 12 & 0x3F);
        s += b64Safe.charAt(c >>  6 & 0x3F);
        s += b64Safe.charAt(c       & 0x3F);
    }
    if (extra > 0) {
        c = this.get(i) << 16;
        if (extra > 1) c |= this.get(i + 1) << 8;
        s += b64Safe.charAt(c >> 18 & 0x3F);
        s += b64Safe.charAt(c >> 12 & 0x3F);
        if (extra == 2) s += b64Safe.charAt(c >> 6 & 0x3F);
    }
    return s;
};
Stream.prototype.isASCII = function (start, end) {
    for (var i = start; i < end; ++i) {
        var c = this.get(i);
        if (c < 32 || c > 176)
            return false;
    }
    return true;
};
Stream.prototype.parseStringISO = function (start, end) {
    var s = "";
    for (var i = start; i < end; ++i)
        s += String.fromCharCode(this.get(i));
    return s;
};
Stream.prototype.parseStringUTF = function (start, end) {
    var s = "";
    for (var i = start; i < end; ) {
        var c = this.get(i++);
        if (c < 128)
            s += String.fromCharCode(c);
        else if ((c > 191) && (c < 224))
            s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F));
        else
            s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F));
    }
    return s;
};
Stream.prototype.parseStringBMP = function (start, end) {
    var str = "", hi, lo;
    for (var i = start; i < end; ) {
        hi = this.get(i++);
        lo = this.get(i++);
        str += String.fromCharCode((hi << 8) | lo);
    }
    return str;
};
Stream.prototype.parseTime = function (start, end, shortYear) {
    var s = this.parseStringISO(start, end),
        m = (shortYear ? reTimeS : reTimeL).exec(s);
    if (!m)
        return "Unrecognized time: " + s;
    if (shortYear) {
        // to avoid querying the timer, use the fixed range [1970, 2069]
        // it will conform with ITU X.400 [-10, +40] sliding window until 2030
        m[1] = +m[1];
        m[1] += (m[1] < 70) ? 2000 : 1900;
    }
    s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4];
    if (m[5]) {
        s += ":" + m[5];
        if (m[6]) {
            s += ":" + m[6];
            if (m[7])
                s += "." + m[7];
        }
    }
    if (m[8]) {
        s += " UTC";
        if (m[8] != 'Z') {
            s += m[8];
            if (m[9])
                s += ":" + m[9];
        }
    }
    return s;
};
Stream.prototype.parseInteger = function (start, end) {
    var v = this.get(start),
        neg = (v > 127),
        pad = neg ? 255 : 0,
        len,
        s = '';
    // skip unuseful bits (not allowed in DER)
    while (v == pad && ++start < end)
        v = this.get(start);
    len = end - start;
    if (len === 0)
        return neg ? '-1' : '0';
    // show bit length of huge integers
    if (len > 4) {
        s = v;
        len <<= 3;
        while (((s ^ pad) & 0x80) == 0) {
            s <<= 1;
            --len;
        }
        s = "(" + len + " bit)\n";
    }
    // decode the integer
    if (neg) v = v - 256;
    var n = new Int10(v);
    for (var i = start + 1; i < end; ++i)
        n.mulAdd(256, this.get(i));
    return s + n.toString();
};
Stream.prototype.parseBitString = function (start, end, maxLength) {
    var unusedBit = this.get(start),
        lenBit = ((end - start - 1) << 3) - unusedBit,
        intro = "(" + lenBit + " bit)\n",
        s = "";
    for (var i = start + 1; i < end; ++i) {
        var b = this.get(i),
            skip = (i == end - 1) ? unusedBit : 0;
        for (var j = 7; j >= skip; --j)
            s += (b >> j) & 1 ? "1" : "0";
        if (s.length > maxLength)
            return intro + stringCut(s, maxLength);
    }
    return intro + s;
};
Stream.prototype.parseOctetString = function (start, end, maxLength) {
    if (this.isASCII(start, end))
        return stringCut(this.parseStringISO(start, end), maxLength);
    var len = end - start,
        s = "(" + len + " byte)\n";
    maxLength /= 2; // we work in bytes
    if (len > maxLength)
        end = start + maxLength;
    for (var i = start; i < end; ++i)
        s += this.hexByte(this.get(i));
    if (len > maxLength)
        s += ellipsis;
    return s;
};
//sunny
Stream.prototype.parseOctetString1 = function (start, end, maxLength) {
    if (this.isASCII(start, end))
        return stringCut(this.parseStringISO(start, end), maxLength);
    var len = end - start,
        s = '';
    maxLength /= 2; // we work in bytes
    if (len > maxLength)
        end = start + maxLength;
    for (var i = start; i < end; ++i)
        s += this.hexByte(this.get(i));
    if (len > maxLength)
        s += ellipsis;
    return s;
};
Stream.prototype.parseOID = function (start, end, maxLength) {
    var s = '',
        n = new Int10(),
        bits = 0;
    for (var i = start; i < end; ++i) {
        var v = this.get(i);
        n.mulAdd(128, v & 0x7F);
        bits += 7;
        if (!(v & 0x80)) { // finished
            if (s === '') {
                n = n.simplify();
                if (n instanceof Int10) {
                    n.sub(80);
                    s = "2." + n.toString();
                } else {
                    var m = n < 80 ? n < 40 ? 0 : 1 : 2;
                    s = m + "." + (n - m * 40);
                }
            } else
                s += "." + n.toString();
            if (s.length > maxLength)
                return stringCut(s, maxLength);
            n = new Int10();
            bits = 0;
        }
    }
    if (bits > 0)
        s += ".incomplete";
    if (typeof oids === 'object') {
        var oid = oids[s];
        if (oid) {
            if (oid.d) s += "\n" + oid.d;
            if (oid.c) s += "\n" + oid.c;
            if (oid.w) s += "\n(warning!)";
        }
    }
    return s;
};

function ASN1(stream, header, length, tag, sub) {
    if (!(tag instanceof ASN1Tag)) throw 'Invalid tag value.';
    this.stream = stream;
    this.header = header;
    this.length = length;
    this.tag = tag;
    this.sub = sub;
}
ASN1.prototype.typeName = function () {
    switch (this.tag.tagClass) {
    case 0: // universal
        switch (this.tag.tagNumber) {
        case 0x00: return "EOC";
        case 0x01: return "BOOLEAN";
        case 0x02: return "INTEGER";
        case 0x03: return "BIT_STRING";
        case 0x04: return "OCTET_STRING";
        case 0x05: return "NULL";
        case 0x06: return "OBJECT_IDENTIFIER";
        case 0x07: return "ObjectDescriptor";
        case 0x08: return "EXTERNAL";
        case 0x09: return "REAL";
        case 0x0A: return "ENUMERATED";
        case 0x0B: return "EMBEDDED_PDV";
        case 0x0C: return "UTF8String";
        case 0x10: return "SEQUENCE";
        case 0x11: return "SET";
        case 0x12: return "NumericString";
        case 0x13: return "PrintableString"; // ASCII subset
        case 0x14: return "TeletexString"; // aka T61String
        case 0x15: return "VideotexString";
        case 0x16: return "IA5String"; // ASCII
        case 0x17: return "UTCTime";
        case 0x18: return "GeneralizedTime";
        case 0x19: return "GraphicString";
        case 0x1A: return "VisibleString"; // ASCII subset
        case 0x1B: return "GeneralString";
        case 0x1C: return "UniversalString";
        case 0x1E: return "BMPString";
        }
        return "Universal_" + this.tag.tagNumber.toString();
    case 1: return "Application_" + this.tag.tagNumber.toString();
    case 2: return "[" + this.tag.tagNumber.toString() + "]"; // Context
    case 3: return "Private_" + this.tag.tagNumber.toString();
    }
};
ASN1.prototype.content = function (maxLength) { // a preview of the content (intended for humans)
    if (this.tag === undefined)
        return null;
    if (maxLength === undefined)
        maxLength = Infinity;
    var content = this.posContent(),
        len = Math.abs(this.length);
    if (!this.tag.isUniversal()) {
        if (this.sub !== null)
            return "(" + this.sub.length + " elem)";
        return this.stream.parseOctetString(content, content + len, maxLength);
    }
    switch (this.tag.tagNumber) {
    case 0x01: // BOOLEAN
        return (this.stream.get(content) === 0) ? "false" : "true";
    case 0x02: // INTEGER
        return this.stream.parseInteger(content, content + len);
    case 0x03: // BIT_STRING
        return this.sub ? "(" + this.sub.length + " elem)" :
            this.stream.parseBitString(content, content + len, maxLength);
    case 0x04: // OCTET_STRING
        return this.sub ? "(" + this.sub.length + " elem)" :
            this.stream.parseOctetString(content, content + len, maxLength);
    //case 0x05: // NULL
    case 0x06: // OBJECT_IDENTIFIER
        return this.stream.parseOID(content, content + len, maxLength);
    //case 0x07: // ObjectDescriptor
    //case 0x08: // EXTERNAL
    //case 0x09: // REAL
    //case 0x0A: // ENUMERATED
    //case 0x0B: // EMBEDDED_PDV
    case 0x10: // SEQUENCE
    case 0x11: // SET
        if (this.sub !== null)
            return "(" + this.sub.length + " elem)";
        else
            return "(no elem)";
    case 0x0C: // UTF8String
        return stringCut(this.stream.parseStringUTF(content, content + len), maxLength);
    case 0x12: // NumericString
    case 0x13: // PrintableString
    case 0x14: // TeletexString
    case 0x15: // VideotexString
    case 0x16: // IA5String
    //case 0x19: // GraphicString
    case 0x1A: // VisibleString
    //case 0x1B: // GeneralString
    //case 0x1C: // UniversalString
        return stringCut(this.stream.parseStringISO(content, content + len), maxLength);
    case 0x1E: // BMPString
        return stringCut(this.stream.parseStringBMP(content, content + len), maxLength);
    case 0x17: // UTCTime
    case 0x18: // GeneralizedTime
        return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17));
    }
    return null;
};
ASN1.prototype.toString = function () {
    return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]";
};
ASN1.prototype.toPrettyString = function (indent) {
    if (indent === undefined) indent = '';
    var s = indent + this.typeName() + " @" + this.stream.pos;
    if (this.length >= 0)
        s += "+";
    s += this.length;
    if (this.tag.tagConstructed)
        s += " (constructed)";
    else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.sub !== null))
        s += " (encapsulates)";
    var content = this.content();
    if (content)
        s += ": " + content.replace(/\n/g, '|');
    s += "\n";
    if (this.sub !== null) {
        indent += '  ';
        for (var i = 0, max = this.sub.length; i < max; ++i)
            s += this.sub[i].toPrettyString(indent);
    }
    return s;
};
ASN1.prototype.posStart = function () {
    return this.stream.pos;
};
ASN1.prototype.posContent = function () {
    return this.stream.pos + this.header;
};
ASN1.prototype.posEnd = function () {
    return this.stream.pos + this.header + Math.abs(this.length);
};
ASN1.prototype.toHexString = function () {
    return this.stream.hexDump(this.posStart(), this.posEnd(), true);
};
ASN1.prototype.toB64String = function () {
    return this.stream.b64Dump(this.posStart(), this.posEnd());
};
ASN1.decodeLength = function (stream) {
    var buf = stream.get(),
        len = buf & 0x7F;
    if (len == buf)
        return len;
    if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways
        throw "Length over 48 bits not supported at position " + (stream.pos - 1);
    if (len === 0)
        return null; // undefined
    buf = 0;
    for (var i = 0; i < len; ++i)
        buf = (buf * 256) + stream.get();
    return buf;
};
function ASN1Tag(stream) {
    var buf = stream.get();
    this.tagClass = buf >> 6;
    this.tagConstructed = ((buf & 0x20) !== 0);
    this.tagNumber = buf & 0x1F;
    if (this.tagNumber == 0x1F) { // long tag
        var n = new Int10();
        do {
            buf = stream.get();
            n.mulAdd(128, buf & 0x7F);
        } while (buf & 0x80);
        this.tagNumber = n.simplify();
    }
}
ASN1Tag.prototype.isUniversal = function () {
    return this.tagClass === 0x00;
};
ASN1Tag.prototype.isEOC = function () {
    return this.tagClass === 0x00 && this.tagNumber === 0x00;
};
ASN1.decode = function (stream) {
    if (!(stream instanceof Stream))
        stream = new Stream(stream, 0);
    var streamStart = new Stream(stream),
        tag = new ASN1Tag(stream),
        len = ASN1.decodeLength(stream),
        start = stream.pos,
        header = start - streamStart.pos,
        sub = null,
        getSub = function () {
            sub = [];
            if (len !== null) {
                // definite length
                var end = start + len;
                if (end > stream.enc.length)
                    throw 'Container at offset ' + start +  ' has a length of ' + len + ', which is past the end of the stream';
                while (stream.pos < end)
                    sub[sub.length] = ASN1.decode(stream);
                if (stream.pos != end)
                    throw 'Content size is not correct for container at offset ' + start;
            } else {
                // undefined length
                try {
                    for (;;) {
                        var s = ASN1.decode(stream);
                        if (s.tag.isEOC())
                            break;
                        sub[sub.length] = s;
                    }
                    len = start - stream.pos; // undefined lengths are represented as negative values
                } catch (e) {
                    throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e;
                }
            }
        };
    if (tag.tagConstructed) {
        // must have valid content
        getSub();
    } else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) {
        // sometimes BitString and OctetString are used to encapsulate ASN.1
        try {
            if (tag.tagNumber == 0x03)
                if (stream.get() != 0)
                    throw "BIT STRINGs with unused bits cannot encapsulate.";
            getSub();
            for (var i = 0; i < sub.length; ++i)
                if (sub[i].tag.isEOC())
                    throw 'EOC is not supposed to be actual content.';
        } catch (e) {
            // but silently ignore when they don't
            sub = null;
            //DEBUG console.log('Could not decode structure at ' + start + ':', e);
        }
    }
    if (sub === null) {
        if (len === null)
            throw "We can't skip over an invalid tag with undefined length at offset " + start;
        stream.pos = start + Math.abs(len);
    }
    return new ASN1(streamStart, header, len, tag, sub);
};

// export globals
if (typeof module !== 'undefined') { module.exports = ASN1; } else { window.ASN1 = ASN1; }
})();
