<?php
/*
 * Copyright 2008-2010 GuardTime AS
 *
 * This file is part of the GuardTime PHP SDK.
 *
 * 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.
 */

/**
 * @package asn1
 */

/**
 * Class implementing DER helper methods for encoding and decoding.
 *
 * @package asn1
 */
class ASN1DER {

    /**
     * Encodes object as DER.
     *
     * @static
     * @throws GTException
     * @param  ASN1DEREncodable $object
     * @return array der encoding bytes
     */
    public static function encode($object) {

        if (!($object instanceof ASN1DEREncodable)) {
            throw new GTException("Unable to encode object that is not DEREncodable");
        }

        return $object->encodeDER();

    }

    /**
     * Encodes the length using DER (L in TLV).
     *
     * @static
     * @param  int $length the length to encode
     * @return array der encoding bytes
     */
    public static function encodeLength($length) {

        $bytes = array();

        if ($length > 127) {

            $size = 1;
            $value = $length;
            while (($value >>= 8) != 0) {
                $size++;
            }

            array_push($bytes, $size | 0x80);

            for ($i = ($size - 1) * 8; $i >= 0; $i -= 8) {

                $byte = $length >> $i;
                $byte = $byte & 0xFF;

                array_push($bytes, $byte);
            }

        } else {

            array_push($bytes, $length);

        }

        return $bytes;

    }

    /**
     * Encodes the type using DER (T in TLV).
     *
     * @static
     * @param  int $type  the type to encode
     * @return array der encoding bytes
     */
    public static function encodeType($type) {

        if ($type == ASN1_TAG_SET || $type == ASN1_TAG_SEQUENCE) {
            $type = $type | 0x20;
        }

        return array($type);
    }

    /**
     * Decodes an ASN1Object from the given byte stream.
     *
     * @static
     * @throws GTException
     * @param  array $bytes the byte stream
     * @return ASN1Object decoded object
     */
    public static function decode($bytes) {

        if (empty($bytes)) {
            throw new GTException("parameter bytes is required");
        }

        $object = ASN1DER::decodeType($bytes);
        $length = ASN1DER::decodeLength($bytes);

        if (count($bytes) < $length) {
            throw new GTException("Invalid DER stream, not enough bytes");
        }

        if (count($bytes) > $length) {
            throw new GTException("Invalid DER stream, too many trailing bytes");
        }

        $object->decodeDER($bytes);

        return $object;
    }

    /**
     * Decodes the length from the given byte stream (L in TLV).
     *
     * @static
     * @throws GTException
     * @param  arrayref &$bytes the byte stream
     * @return int decoded length
     */
    public static function decodeLength(&$bytes) {

        if (!is_array($bytes)) {
            throw new GTException("parameter bytes must be an array of bytes");
        }

        if (count($bytes) == 0) {
            throw new GTException("Invalid DER stream, not enough bytes to decode length");
        }

        $length = array_shift($bytes);

        if ($length == 128) {
            throw new GTException("indefinite length encoding not allowed for DER");
        }

        if ($length > 127) {

            $size = $length & 0x7F;

            if ($size > 4) {
                throw new GTException("size > 4");
            }

            $length = 0;

            for ($i = 0; $i < $size; $i++) {
                if (count($bytes) == 0) {
                    throw new GTException("Invalid DER stream, not enough bytes to decode length");
                }

                $length = ($length << 8) + array_shift($bytes);
            }

        }

        return $length;

    }

    /**
     * Decodes type from the given byte stream (T in TLV).
     *
     * @static
     * @throws GTException
     * @param  arrayref &$bytes the byte stream
     * @return ASN1Object a concrete subclass instance
     */
    public static function decodeType(&$bytes) {

        if (!is_array($bytes)) {
            throw new GTException("parameter bytes must be an array of bytes");
        }

        if (count($bytes) < 1) {
            throw new GTException("unexpected EOF");
        }

        $byte = array_shift($bytes);

        $bit8 = ($byte >> 7) & 0x1;
        $bit7 = ($byte >> 6) & 0x1;
        $bit6 = ($byte >> 5) & 0x1;

        $tagClass = "";

        if ($bit8 == 0 && $bit7 == 0) {

            // 00xx xxxx UNIVERSAL
            $tagClass = ASN1_TAG_UNIVERSAL;

        } else if ($bit8 == 0 && $bit7 == 1) {

            // 01xx xxxx APPLICATION
            $tagClass = ASN1_TAG_APPLICATION;

        } else if ($bit8 == 1 && $bit7 == 0) {

            // 10xx xxxx CONTEXT
            $tagClass = ASN1_TAG_CONTEXT;

        } else if ($bit7 == 1 && $bit8 == 1) {

            // 11xx xxxx PRIVATE
            $tagClass = ASN1_TAG_PRIVATE;

        }

        if ($bit6) {

            // xx1x xxxx CONSTRUCTED
            $tagType = ASN1_TAG_CONSTRUCTED;

        } else {

            // xx0x xxxx PRIMITIVE
            $tagType = ASN1_TAG_PRIMITIVE;

        }

        $byte = $byte & 0x1F; // clear tags: 000x xxxx

        if ($byte == 0x1F) {
            throw new GTException("Multibyte tags not yet supported!");
        }

        $object = null;

        if ($tagClass == ASN1_TAG_UNIVERSAL) {

            switch ($byte) {

                case ASN1_TAG_BOOLEAN:

                    $object = new ASN1Boolean();
                    break;

                case ASN1_TAG_INTEGER:

                    $object = new ASN1Integer();
                    break;

                case ASN1_TAG_BIT_STRING:

                    $object = new ASN1BitString();
                    break;

                case ASN1_TAG_OCTET_STRING:

                    $object = new ASN1OctetString();
                    break;

                case ASN1_TAG_NULL:

                    $object = new ASN1Null();
                    break;

                case ASN1_TAG_OBJECT_ID:

                    $object = new ASN1ObjectId();
                    break;

                case ASN1_TAG_UTF8_STRING:

                    $object = new ASN1UTF8String();
                    break;

                case ASN1_TAG_PRINTABLE_STRING:

                    $object = new ASN1PrintableString();
                    break;

                case ASN1_TAG_T61_STRING:

                    $object = new ASN1T61String();
                    break;

                case ASN1_TAG_IA5_STRING:

                    $object = new ASN1IA5String();
                    break;

                case ASN1_TAG_BMP_STRING:

                    $object = new ASN1BMPString();
                    break;

                case ASN1_TAG_UTC_TIME:

                    $object = new ASN1UTCTime();
                    break;

                case ASN1_TAG_GENERALIZED_TIME:

                    $object = new ASN1GeneralizedTime();
                    break;

                case ASN1_TAG_SEQUENCE:

                    $object = new ASN1Sequence();
                    break;

                case ASN1_TAG_SET:

                    $object = new ASN1Set();
                    break;

                default:

                    throw new GTException("Unsupported ASN.1 UNIVERSAL type: {$byte}");
            }

        } else {

            $object = new ASN1Tag();
        }

        $object->setTagClass($tagClass);
        $object->setTagType($tagType);
        $object->setTagValue($byte);

        return $object;

    }

}

?>
