"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASN1Obj = void 0;
/*
Copyright 2023 The Sigstore Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const stream_1 = require("../stream");
const error_1 = require("./error");
const length_1 = require("./length");
const parse_1 = require("./parse");
const tag_1 = require("./tag");
class ASN1Obj {
    tag;
    subs;
    value;
    constructor(tag, value, subs) {
        this.tag = tag;
        this.value = value;
        this.subs = subs;
    }
    // Constructs an ASN.1 object from a Buffer of DER-encoded bytes.
    static parseBuffer(buf) {
        const stream = new stream_1.ByteStream(buf);
        const obj = parseStream(stream);
        // Ensure the entire buffer was consumed; trailing data after the top-level
        // object indicates a malformed (or maliciously padded) encoding.
        if (stream.position !== stream.length) {
            throw new error_1.ASN1ParseError('invalid trailing data');
        }
        return obj;
    }
    toDER() {
        const valueStream = new stream_1.ByteStream();
        if (this.subs.length > 0) {
            for (const sub of this.subs) {
                valueStream.appendView(sub.toDER());
            }
        }
        else {
            valueStream.appendView(this.value);
        }
        const value = valueStream.buffer;
        // Concat tag/length/value
        const obj = new stream_1.ByteStream();
        obj.appendChar(this.tag.toDER());
        obj.appendView((0, length_1.encodeLength)(value.length));
        obj.appendView(value);
        return obj.buffer;
    }
    /////////////////////////////////////////////////////////////////////////////
    // Convenience methods for parsing ASN.1 primitives into JS types
    // Returns the ASN.1 object's value as a boolean. Throws an error if the
    // object is not a boolean.
    toBoolean() {
        if (!this.tag.isBoolean()) {
            throw new error_1.ASN1TypeError('not a boolean');
        }
        return (0, parse_1.parseBoolean)(this.value);
    }
    // Returns the ASN.1 object's value as a BigInt. Throws an error if the
    // object is not an integer.
    toInteger() {
        if (!this.tag.isInteger()) {
            throw new error_1.ASN1TypeError('not an integer');
        }
        return (0, parse_1.parseInteger)(this.value);
    }
    // Returns the ASN.1 object's value as an OID string. Throws an error if the
    // object is not an OID.
    toOID() {
        if (!this.tag.isOID()) {
            throw new error_1.ASN1TypeError('not an OID');
        }
        return (0, parse_1.parseOID)(this.value);
    }
    // Returns the ASN.1 object's value as a Date. Throws an error if the object
    // is not either a UTCTime or a GeneralizedTime.
    toDate() {
        switch (true) {
            case this.tag.isUTCTime():
                return (0, parse_1.parseTime)(this.value, true);
            case this.tag.isGeneralizedTime():
                return (0, parse_1.parseTime)(this.value, false);
            default:
                throw new error_1.ASN1TypeError('not a date');
        }
    }
    // Returns the ASN.1 object's value as a number[] where each number is the
    // value of a bit in the bit string. Throws an error if the object is not a
    // bit string.
    toBitString() {
        if (!this.tag.isBitString()) {
            throw new error_1.ASN1TypeError('not a bit string');
        }
        return (0, parse_1.parseBitString)(this.value);
    }
}
exports.ASN1Obj = ASN1Obj;
/////////////////////////////////////////////////////////////////////////////
// Internal stream parsing functions
// Maximum nesting depth for parsed ASN.1 objects. Bounds the mutual recursion
// between parseStream and collectSubs so that deeply nested DER cannot exhaust
// the call stack (denial of service).
const MAX_DEPTH = 100;
function parseStream(stream, depth = 0) {
    if (depth > MAX_DEPTH) {
        throw new error_1.ASN1ParseError('maximum nesting depth exceeded');
    }
    // Parse tag, length, and value from stream
    const tag = new tag_1.ASN1Tag(stream.getUint8());
    const len = (0, length_1.decodeLength)(stream);
    const value = stream.slice(stream.position, len);
    const start = stream.position;
    let subs = [];
    // If the object is constructed, parse its children. Sometimes, children
    // are embedded in OCTESTRING objects, so we need to check those
    // for children as well.
    if (tag.constructed) {
        subs = collectSubs(stream, len, depth);
    }
    else if (tag.isOctetString()) {
        // Attempt to parse children of OCTETSTRING objects. If anything fails,
        // assume the object is not constructed and treat as primitive. This is
        // intentional: it transparently unwraps DER content embedded in an OCTET
        // STRING (e.g. X.509 extnValue, CMS eContent). The error is swallowed
        // because a parse failure simply means the bytes are an opaque primitive
        // value rather than a nested structure.
        try {
            subs = collectSubs(stream, len, depth);
        }
        catch (e) {
            // Fail silently and treat as primitive
        }
    }
    // If there are no children, move stream cursor to the end of the object
    if (subs.length === 0) {
        stream.seek(start + len);
    }
    return new ASN1Obj(tag, value, subs);
}
function collectSubs(stream, len, depth) {
    // Calculate end of object content
    const end = stream.position + len;
    // Make sure there are enough bytes left in the stream. This should never
    // happen, cause it'll get caught when the stream is sliced in parseStream.
    // Leaving as an extra check just in case.
    /* istanbul ignore if */
    if (end > stream.length) {
        throw new error_1.ASN1ParseError('invalid length');
    }
    // Parse all children
    const subs = [];
    while (stream.position < end) {
        subs.push(parseStream(stream, depth + 1));
    }
    // When we're done parsing children, we should be at the end of the object
    if (stream.position !== end) {
        throw new error_1.ASN1ParseError('invalid length');
    }
    return subs;
}
