xref: /plugin/pureldap/vendor/freedsx/asn1/src/FreeDSx/Asn1/Encoder/BerEncoder.php (revision dad993c57a70866aa1db59c43f043769c2eb7ed0)
10b3fd2d3SAndreas Gohr<?php
20b3fd2d3SAndreas Gohr/**
30b3fd2d3SAndreas Gohr * This file is part of the FreeDSx ASN1 package.
40b3fd2d3SAndreas Gohr *
50b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
60b3fd2d3SAndreas Gohr *
70b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE
80b3fd2d3SAndreas Gohr * file that was distributed with this source code.
90b3fd2d3SAndreas Gohr */
100b3fd2d3SAndreas Gohr
110b3fd2d3SAndreas Gohrnamespace FreeDSx\Asn1\Encoder;
120b3fd2d3SAndreas Gohr
13*dad993c5SAndreas Gohruse DateTime;
14*dad993c5SAndreas Gohruse DateTimeZone;
150b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Exception\EncoderException;
160b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Exception\InvalidArgumentException;
170b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Exception\PartialPduException;
180b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\AbstractStringType;
190b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\AbstractTimeType;
200b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\AbstractType;
21*dad993c5SAndreas Gohruse FreeDSx\Asn1\Type as EncodedType;
220b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\BitStringType;
230b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\BooleanType;
240b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\EnumeratedType;
250b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\GeneralizedTimeType;
260b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\IncompleteType;
270b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\IntegerType;
280b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\NullType;
290b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\OidType;
300b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\RealType;
310b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\RelativeOidType;
320b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\SetOfType;
330b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\SetType;
340b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Type\UtcTimeType;
35*dad993c5SAndreas Gohruse function bin2hex;
36*dad993c5SAndreas Gohruse function bindec;
37*dad993c5SAndreas Gohruse function chr;
38*dad993c5SAndreas Gohruse function count;
39*dad993c5SAndreas Gohruse function decbin;
40*dad993c5SAndreas Gohruse function dechex;
41*dad993c5SAndreas Gohruse function explode;
42*dad993c5SAndreas Gohruse function extension_loaded;
43*dad993c5SAndreas Gohruse function floor;
44*dad993c5SAndreas Gohruse function gmp_abs;
45*dad993c5SAndreas Gohruse function gmp_add;
46*dad993c5SAndreas Gohruse function gmp_and;
47*dad993c5SAndreas Gohruse function gmp_div;
48*dad993c5SAndreas Gohruse function gmp_export;
49*dad993c5SAndreas Gohruse function gmp_import;
50*dad993c5SAndreas Gohruse function gmp_init;
51*dad993c5SAndreas Gohruse function gmp_intval;
52*dad993c5SAndreas Gohruse function gmp_mul;
53*dad993c5SAndreas Gohruse function gmp_neg;
54*dad993c5SAndreas Gohruse function gmp_or;
55*dad993c5SAndreas Gohruse function gmp_pow;
56*dad993c5SAndreas Gohruse function gmp_strval;
57*dad993c5SAndreas Gohruse function gmp_sub;
58*dad993c5SAndreas Gohruse function hex2bin;
59*dad993c5SAndreas Gohruse function hexdec;
60*dad993c5SAndreas Gohruse function is_float;
61*dad993c5SAndreas Gohruse function is_int;
62*dad993c5SAndreas Gohruse function is_numeric;
63*dad993c5SAndreas Gohruse function is_string;
64*dad993c5SAndreas Gohruse function ord;
65*dad993c5SAndreas Gohruse function preg_match;
66*dad993c5SAndreas Gohruse function rtrim;
67*dad993c5SAndreas Gohruse function sprintf;
68*dad993c5SAndreas Gohruse function str_pad;
69*dad993c5SAndreas Gohruse function strlen;
70*dad993c5SAndreas Gohruse function strpos;
71*dad993c5SAndreas Gohruse function substr;
720b3fd2d3SAndreas Gohr
730b3fd2d3SAndreas Gohr/**
740b3fd2d3SAndreas Gohr * Basic Encoding Rules (BER) encoder.
750b3fd2d3SAndreas Gohr *
760b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
770b3fd2d3SAndreas Gohr */
780b3fd2d3SAndreas Gohrclass BerEncoder implements EncoderInterface
790b3fd2d3SAndreas Gohr{
800b3fd2d3SAndreas Gohr    /**
810b3fd2d3SAndreas Gohr     * Used to represent a bool false binary string.
820b3fd2d3SAndreas Gohr     */
830b3fd2d3SAndreas Gohr    protected const BOOL_FALSE = "\x00";
840b3fd2d3SAndreas Gohr
850b3fd2d3SAndreas Gohr    /**
860b3fd2d3SAndreas Gohr     * Used to represent a bool true binary string.
870b3fd2d3SAndreas Gohr     */
880b3fd2d3SAndreas Gohr    protected const BOOL_TRUE = "\xff";
890b3fd2d3SAndreas Gohr
900b3fd2d3SAndreas Gohr    /**
91fd0855ecSAndreas Gohr     * Anything greater than this we assume we may need to deal with a bigint in an OIDs second component.
92fd0855ecSAndreas Gohr     */
93fd0855ecSAndreas Gohr    protected const MAX_SECOND_COMPONENT = PHP_INT_MAX - 80;
94fd0855ecSAndreas Gohr
95fd0855ecSAndreas Gohr    /**
960b3fd2d3SAndreas Gohr     * @var array
970b3fd2d3SAndreas Gohr     */
980b3fd2d3SAndreas Gohr    protected $tagMap = [
990b3fd2d3SAndreas Gohr        AbstractType::TAG_CLASS_APPLICATION => [],
1000b3fd2d3SAndreas Gohr        AbstractType::TAG_CLASS_CONTEXT_SPECIFIC => [],
1010b3fd2d3SAndreas Gohr        AbstractType::TAG_CLASS_PRIVATE => [],
1020b3fd2d3SAndreas Gohr    ];
1030b3fd2d3SAndreas Gohr
1040b3fd2d3SAndreas Gohr    protected $tmpTagMap = [];
1050b3fd2d3SAndreas Gohr
1060b3fd2d3SAndreas Gohr    /**
1070b3fd2d3SAndreas Gohr     * @var array
1080b3fd2d3SAndreas Gohr     */
1090b3fd2d3SAndreas Gohr    protected $options = [
1100b3fd2d3SAndreas Gohr        'bitstring_padding' => '0',
1110b3fd2d3SAndreas Gohr    ];
1120b3fd2d3SAndreas Gohr
1130b3fd2d3SAndreas Gohr    /**
1140b3fd2d3SAndreas Gohr     * @var bool
1150b3fd2d3SAndreas Gohr     */
1160b3fd2d3SAndreas Gohr    protected $isGmpAvailable;
1170b3fd2d3SAndreas Gohr
1180b3fd2d3SAndreas Gohr    /**
1190b3fd2d3SAndreas Gohr     * @var int
1200b3fd2d3SAndreas Gohr     */
1210b3fd2d3SAndreas Gohr    protected $pos;
1220b3fd2d3SAndreas Gohr
1230b3fd2d3SAndreas Gohr    /**
1240b3fd2d3SAndreas Gohr     * @var int|null
1250b3fd2d3SAndreas Gohr     */
1260b3fd2d3SAndreas Gohr    protected $lastPos;
1270b3fd2d3SAndreas Gohr
1280b3fd2d3SAndreas Gohr    /**
1290b3fd2d3SAndreas Gohr     * @var int
1300b3fd2d3SAndreas Gohr     */
1310b3fd2d3SAndreas Gohr    protected $maxLen;
1320b3fd2d3SAndreas Gohr
1330b3fd2d3SAndreas Gohr    /**
134fd0855ecSAndreas Gohr     * @var string|null
1350b3fd2d3SAndreas Gohr     */
1360b3fd2d3SAndreas Gohr    protected $binary;
1370b3fd2d3SAndreas Gohr
1380b3fd2d3SAndreas Gohr    /**
1390b3fd2d3SAndreas Gohr     * @param array $options
1400b3fd2d3SAndreas Gohr     */
1410b3fd2d3SAndreas Gohr    public function __construct(array $options = [])
1420b3fd2d3SAndreas Gohr    {
143*dad993c5SAndreas Gohr        $this->isGmpAvailable = extension_loaded('gmp');
1440b3fd2d3SAndreas Gohr        $this->setOptions($options);
1450b3fd2d3SAndreas Gohr    }
1460b3fd2d3SAndreas Gohr
1470b3fd2d3SAndreas Gohr    /**
1480b3fd2d3SAndreas Gohr     * {@inheritdoc}
1490b3fd2d3SAndreas Gohr     */
1500b3fd2d3SAndreas Gohr    public function decode($binary, array $tagMap = []): AbstractType
1510b3fd2d3SAndreas Gohr    {
1520b3fd2d3SAndreas Gohr        $this->startEncoding($binary, $tagMap);
1530b3fd2d3SAndreas Gohr        if ($this->maxLen === 0) {
1540b3fd2d3SAndreas Gohr            throw new InvalidArgumentException('The data to decode cannot be empty.');
1550b3fd2d3SAndreas Gohr        } elseif ($this->maxLen === 1) {
1560b3fd2d3SAndreas Gohr            throw new PartialPduException('Received only 1 byte of data.');
1570b3fd2d3SAndreas Gohr        }
1580b3fd2d3SAndreas Gohr        $type = $this->decodeBytes(true);
1590b3fd2d3SAndreas Gohr        $this->stopEncoding();
1600b3fd2d3SAndreas Gohr
1610b3fd2d3SAndreas Gohr        return $type;
1620b3fd2d3SAndreas Gohr    }
1630b3fd2d3SAndreas Gohr
1640b3fd2d3SAndreas Gohr    /**
1650b3fd2d3SAndreas Gohr     * {@inheritdoc}
1660b3fd2d3SAndreas Gohr     */
1670b3fd2d3SAndreas Gohr    public function complete(IncompleteType $type, int $tagType, array $tagMap = []): AbstractType
1680b3fd2d3SAndreas Gohr    {
169fd0855ecSAndreas Gohr        $lastPos = $this->lastPos;
1700b3fd2d3SAndreas Gohr        $this->startEncoding($type->getValue(), $tagMap);
1710b3fd2d3SAndreas Gohr        $newType = $this->decodeBytes(false, $tagType, $this->maxLen, $type->getIsConstructed(), AbstractType::TAG_CLASS_UNIVERSAL);
1720b3fd2d3SAndreas Gohr        $this->stopEncoding();
1730b3fd2d3SAndreas Gohr        $newType->setTagNumber($type->getTagNumber())
1740b3fd2d3SAndreas Gohr            ->setTagClass($type->getTagClass());
175fd0855ecSAndreas Gohr        $this->lastPos = $lastPos;
1760b3fd2d3SAndreas Gohr
1770b3fd2d3SAndreas Gohr        return $newType;
1780b3fd2d3SAndreas Gohr    }
1790b3fd2d3SAndreas Gohr
1800b3fd2d3SAndreas Gohr    /**
1810b3fd2d3SAndreas Gohr     * {@inheritdoc}
1820b3fd2d3SAndreas Gohr     */
1830b3fd2d3SAndreas Gohr    public function encode(AbstractType $type): string
1840b3fd2d3SAndreas Gohr    {
1850b3fd2d3SAndreas Gohr        switch ($type) {
1860b3fd2d3SAndreas Gohr            case $type instanceof BooleanType:
1870b3fd2d3SAndreas Gohr                $bytes = $type->getValue() ? self::BOOL_TRUE : self::BOOL_FALSE;
1880b3fd2d3SAndreas Gohr                break;
1890b3fd2d3SAndreas Gohr            case $type instanceof IntegerType:
1900b3fd2d3SAndreas Gohr            case $type instanceof EnumeratedType:
1910b3fd2d3SAndreas Gohr                $bytes = $this->encodeInteger($type);
1920b3fd2d3SAndreas Gohr                break;
1930b3fd2d3SAndreas Gohr            case $type instanceof RealType:
1940b3fd2d3SAndreas Gohr                $bytes = $this->encodeReal($type);
1950b3fd2d3SAndreas Gohr                break;
1960b3fd2d3SAndreas Gohr            case $type instanceof AbstractStringType:
1970b3fd2d3SAndreas Gohr                $bytes = $type->getValue();
1980b3fd2d3SAndreas Gohr                break;
1990b3fd2d3SAndreas Gohr            case $type instanceof SetOfType:
2000b3fd2d3SAndreas Gohr                $bytes = $this->encodeSetOf($type);
2010b3fd2d3SAndreas Gohr                break;
2020b3fd2d3SAndreas Gohr            case $type instanceof SetType:
2030b3fd2d3SAndreas Gohr                $bytes = $this->encodeSet($type);
2040b3fd2d3SAndreas Gohr                break;
2050b3fd2d3SAndreas Gohr            case $type->getIsConstructed():
2060b3fd2d3SAndreas Gohr                $bytes = $this->encodeConstructedType(...$type->getChildren());
2070b3fd2d3SAndreas Gohr                break;
2080b3fd2d3SAndreas Gohr            case $type instanceof BitStringType:
2090b3fd2d3SAndreas Gohr                $bytes = $this->encodeBitString($type);
2100b3fd2d3SAndreas Gohr                break;
2110b3fd2d3SAndreas Gohr            case $type instanceof OidType:
2120b3fd2d3SAndreas Gohr                $bytes = $this->encodeOid($type);
2130b3fd2d3SAndreas Gohr                break;
2140b3fd2d3SAndreas Gohr            case $type instanceof RelativeOidType:
2150b3fd2d3SAndreas Gohr                $bytes = $this->encodeRelativeOid($type);
2160b3fd2d3SAndreas Gohr                break;
2170b3fd2d3SAndreas Gohr            case $type instanceof GeneralizedTimeType:
2180b3fd2d3SAndreas Gohr                $bytes = $this->encodeGeneralizedTime($type);
2190b3fd2d3SAndreas Gohr                break;
2200b3fd2d3SAndreas Gohr            case $type instanceof UtcTimeType:
2210b3fd2d3SAndreas Gohr                $bytes = $this->encodeUtcTime($type);
2220b3fd2d3SAndreas Gohr                break;
2230b3fd2d3SAndreas Gohr            case $type instanceof NullType:
2240b3fd2d3SAndreas Gohr                $bytes = '';
2250b3fd2d3SAndreas Gohr                break;
2260b3fd2d3SAndreas Gohr            default:
227fd0855ecSAndreas Gohr                throw new EncoderException(sprintf(
228fd0855ecSAndreas Gohr                    'The type "%s" is not currently supported.',
229fd0855ecSAndreas Gohr                    get_class($type)
230fd0855ecSAndreas Gohr                ));
2310b3fd2d3SAndreas Gohr        }
232*dad993c5SAndreas Gohr        $length = strlen($bytes);
233*dad993c5SAndreas Gohr        $bytes = ($length < 128) ? chr($length) . $bytes : $this->encodeLongDefiniteLength($length) . $bytes;
2340b3fd2d3SAndreas Gohr
2350b3fd2d3SAndreas Gohr        # The first byte of a tag always contains the class (bits 8 and 7) and whether it is constructed (bit 6).
2360b3fd2d3SAndreas Gohr        $tag = $type->getTagClass() | ($type->getIsConstructed() ? AbstractType::CONSTRUCTED_TYPE : 0);
2370b3fd2d3SAndreas Gohr
2380b3fd2d3SAndreas Gohr        $this->validateNumericInt($type->getTagNumber());
2390b3fd2d3SAndreas Gohr        # For a high tag (>=31) we flip the first 5 bits on (0x1f) to make the first byte, then the subsequent bytes is
2400b3fd2d3SAndreas Gohr        # the VLV encoding of the tag number.
2410b3fd2d3SAndreas Gohr        if ($type->getTagNumber() >= 31) {
242*dad993c5SAndreas Gohr            $bytes = chr($tag | 0x1f) . $this->intToVlqBytes($type->getTagNumber()) . $bytes;
2430b3fd2d3SAndreas Gohr        # For a tag less than 31, everything fits comfortably into a single byte.
2440b3fd2d3SAndreas Gohr        } else {
245*dad993c5SAndreas Gohr            $bytes = chr($tag | $type->getTagNumber()) . $bytes;
2460b3fd2d3SAndreas Gohr        }
2470b3fd2d3SAndreas Gohr
2480b3fd2d3SAndreas Gohr        return $bytes;
2490b3fd2d3SAndreas Gohr    }
2500b3fd2d3SAndreas Gohr
2510b3fd2d3SAndreas Gohr    /**
2520b3fd2d3SAndreas Gohr     * Map universal types to specific tag class values when decoding.
2530b3fd2d3SAndreas Gohr     *
2540b3fd2d3SAndreas Gohr     * @param int $class
2550b3fd2d3SAndreas Gohr     * @param array $map
2560b3fd2d3SAndreas Gohr     * @return $this
2570b3fd2d3SAndreas Gohr     */
2580b3fd2d3SAndreas Gohr    public function setTagMap(int $class, array $map)
2590b3fd2d3SAndreas Gohr    {
2600b3fd2d3SAndreas Gohr        if (isset($this->tagMap[$class])) {
2610b3fd2d3SAndreas Gohr            $this->tagMap[$class] = $map;
2620b3fd2d3SAndreas Gohr        }
2630b3fd2d3SAndreas Gohr
2640b3fd2d3SAndreas Gohr        return $this;
2650b3fd2d3SAndreas Gohr    }
2660b3fd2d3SAndreas Gohr
2670b3fd2d3SAndreas Gohr    /**
2680b3fd2d3SAndreas Gohr     * Get the options for the encoder.
2690b3fd2d3SAndreas Gohr     *
2700b3fd2d3SAndreas Gohr     * @return array
2710b3fd2d3SAndreas Gohr     */
2720b3fd2d3SAndreas Gohr    public function getOptions(): array
2730b3fd2d3SAndreas Gohr    {
2740b3fd2d3SAndreas Gohr        return $this->options;
2750b3fd2d3SAndreas Gohr    }
2760b3fd2d3SAndreas Gohr
2770b3fd2d3SAndreas Gohr    /**
2780b3fd2d3SAndreas Gohr     * Set the options for the encoder.
2790b3fd2d3SAndreas Gohr     *
2800b3fd2d3SAndreas Gohr     * @param array $options
2810b3fd2d3SAndreas Gohr     * @return $this
2820b3fd2d3SAndreas Gohr     */
2830b3fd2d3SAndreas Gohr    public function setOptions(array $options)
2840b3fd2d3SAndreas Gohr    {
285*dad993c5SAndreas Gohr        if (isset($options['bitstring_padding']) && is_string($options['bitstring_padding'])) {
2860b3fd2d3SAndreas Gohr            $this->options['bitstring_padding'] = $options['bitstring_padding'];
2870b3fd2d3SAndreas Gohr        }
2880b3fd2d3SAndreas Gohr
2890b3fd2d3SAndreas Gohr        return $this;
2900b3fd2d3SAndreas Gohr    }
2910b3fd2d3SAndreas Gohr
2920b3fd2d3SAndreas Gohr    /**
2930b3fd2d3SAndreas Gohr     * @return int|null
2940b3fd2d3SAndreas Gohr     */
2950b3fd2d3SAndreas Gohr    public function getLastPosition(): ?int
2960b3fd2d3SAndreas Gohr    {
2970b3fd2d3SAndreas Gohr        return $this->lastPos;
2980b3fd2d3SAndreas Gohr    }
2990b3fd2d3SAndreas Gohr
3000b3fd2d3SAndreas Gohr    protected function startEncoding(string $binary, array $tagMap): void
3010b3fd2d3SAndreas Gohr    {
3020b3fd2d3SAndreas Gohr        $this->tmpTagMap = $tagMap + $this->tagMap;
3030b3fd2d3SAndreas Gohr        $this->binary = $binary;
3040b3fd2d3SAndreas Gohr        $this->lastPos = null;
3050b3fd2d3SAndreas Gohr        $this->pos = 0;
306*dad993c5SAndreas Gohr        $this->maxLen = strlen($this->binary);
3070b3fd2d3SAndreas Gohr    }
3080b3fd2d3SAndreas Gohr
3090b3fd2d3SAndreas Gohr    protected function stopEncoding(): void
3100b3fd2d3SAndreas Gohr    {
3110b3fd2d3SAndreas Gohr        $this->tmpTagMap = [];
3120b3fd2d3SAndreas Gohr        $this->binary = null;
3130b3fd2d3SAndreas Gohr        $this->maxLen = 0;
3140b3fd2d3SAndreas Gohr        $this->lastPos = $this->pos;
3150b3fd2d3SAndreas Gohr        $this->pos = 0;
3160b3fd2d3SAndreas Gohr    }
3170b3fd2d3SAndreas Gohr
3180b3fd2d3SAndreas Gohr    /**
3190b3fd2d3SAndreas Gohr     * @param bool $isRoot
3200b3fd2d3SAndreas Gohr     * @param null|int $tagType
3210b3fd2d3SAndreas Gohr     * @param null|int $length
3220b3fd2d3SAndreas Gohr     * @param null|bool $isConstructed
3230b3fd2d3SAndreas Gohr     * @param null|int $class
3240b3fd2d3SAndreas Gohr     * @return AbstractType
3250b3fd2d3SAndreas Gohr     * @throws EncoderException
3260b3fd2d3SAndreas Gohr     * @throws PartialPduException
3270b3fd2d3SAndreas Gohr     */
3280b3fd2d3SAndreas Gohr    protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = null, $isConstructed = null, $class = null): AbstractType
3290b3fd2d3SAndreas Gohr    {
3300b3fd2d3SAndreas Gohr        $tagNumber = $tagType;
3310b3fd2d3SAndreas Gohr        if ($tagType === null) {
332*dad993c5SAndreas Gohr            $tag = ord($this->binary[$this->pos++]);
3330b3fd2d3SAndreas Gohr            $class = $tag & 0xc0;
3340b3fd2d3SAndreas Gohr            $isConstructed = (bool)($tag & AbstractType::CONSTRUCTED_TYPE);
3350b3fd2d3SAndreas Gohr            $tagNumber = $tag & ~0xe0;
3360b3fd2d3SAndreas Gohr
3370b3fd2d3SAndreas Gohr            # Less than or equal to 30 is a low tag number represented in a single byte.
3380b3fd2d3SAndreas Gohr            # A high tag number is determined using VLQ (like the OID identifier encoding) of the subsequent bytes.
3390b3fd2d3SAndreas Gohr            if ($tagNumber > 30) {
3400b3fd2d3SAndreas Gohr                try {
3410b3fd2d3SAndreas Gohr                    $tagNumber = $this->getVlqBytesToInt();
3420b3fd2d3SAndreas Gohr                    # It's possible we only got part of the VLQ for the high tag, as there is no way to know its ending length.
3430b3fd2d3SAndreas Gohr                } catch (EncoderException $e) {
3440b3fd2d3SAndreas Gohr                    if ($isRoot) {
3450b3fd2d3SAndreas Gohr                        throw new PartialPduException(
3460b3fd2d3SAndreas Gohr                            'Not enough data to decode the high tag number. No ending byte encountered for the VLQ bytes.'
3470b3fd2d3SAndreas Gohr                        );
3480b3fd2d3SAndreas Gohr                    }
3490b3fd2d3SAndreas Gohr                    throw $e;
3500b3fd2d3SAndreas Gohr                }
3510b3fd2d3SAndreas Gohr            }
3520b3fd2d3SAndreas Gohr
353*dad993c5SAndreas Gohr            $length = ord($this->binary[$this->pos++]);
3540b3fd2d3SAndreas Gohr            if ($length === 128) {
3550b3fd2d3SAndreas Gohr                throw new EncoderException('Indefinite length encoding is not currently supported.');
3560b3fd2d3SAndreas Gohr            }
3570b3fd2d3SAndreas Gohr            if ($length > 128) {
3580b3fd2d3SAndreas Gohr                $length = $this->decodeLongDefiniteLength($length);
3590b3fd2d3SAndreas Gohr            }
3600b3fd2d3SAndreas Gohr            $tagType = ($class === AbstractType::TAG_CLASS_UNIVERSAL) ? $tagNumber : ($this->tmpTagMap[$class][$tagNumber] ?? null);
3610b3fd2d3SAndreas Gohr
3620b3fd2d3SAndreas Gohr            if (($this->maxLen - $this->pos) < $length) {
3630b3fd2d3SAndreas Gohr                $message = sprintf(
3640b3fd2d3SAndreas Gohr                    'The expected byte length was %s, but received %s.',
3650b3fd2d3SAndreas Gohr                    $length,
3660b3fd2d3SAndreas Gohr                    ($this->maxLen - $this->pos)
3670b3fd2d3SAndreas Gohr                );
3680b3fd2d3SAndreas Gohr                if ($isRoot) {
3690b3fd2d3SAndreas Gohr                    throw new PartialPduException($message);
3700b3fd2d3SAndreas Gohr                } else {
3710b3fd2d3SAndreas Gohr                    throw new EncoderException($message);
3720b3fd2d3SAndreas Gohr                }
3730b3fd2d3SAndreas Gohr            }
3740b3fd2d3SAndreas Gohr
3750b3fd2d3SAndreas Gohr            if ($tagType === null) {
376*dad993c5SAndreas Gohr                $type = new IncompleteType(substr($this->binary, $this->pos, $length), $tagNumber, $class, $isConstructed);
3770b3fd2d3SAndreas Gohr                $this->pos += $length;
3780b3fd2d3SAndreas Gohr
3790b3fd2d3SAndreas Gohr                return $type;
3800b3fd2d3SAndreas Gohr            }
3810b3fd2d3SAndreas Gohr        }
3820b3fd2d3SAndreas Gohr
3830b3fd2d3SAndreas Gohr        # Yes...this huge switch statement should be a separate method. However, it is faster inline when decoding
3840b3fd2d3SAndreas Gohr        # lots of data (such as thousands of ASN.1 structures at a time).
3850b3fd2d3SAndreas Gohr        switch ($tagType) {
3860b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_BOOLEAN:
3870b3fd2d3SAndreas Gohr                if ($length !== 1 || $isConstructed) {
3880b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded boolean type is malformed.');
3890b3fd2d3SAndreas Gohr                }
3900b3fd2d3SAndreas Gohr                $type = EncodedType\BooleanType::withTag($tagNumber, $class, $this->decodeBoolean());
3910b3fd2d3SAndreas Gohr                break;
3920b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_NULL:
3930b3fd2d3SAndreas Gohr                if ($length !== 0 || $isConstructed) {
3940b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded null type is malformed.');
3950b3fd2d3SAndreas Gohr                }
3960b3fd2d3SAndreas Gohr                $type = EncodedType\NullType::withTag($tagNumber, $class);
3970b3fd2d3SAndreas Gohr                break;
3980b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_INTEGER:
3990b3fd2d3SAndreas Gohr                if ($isConstructed) {
4000b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded integer type is malformed.');
4010b3fd2d3SAndreas Gohr                }
4020b3fd2d3SAndreas Gohr                $type = EncodedType\IntegerType::withTag($tagNumber, $class, $this->decodeInteger($length));
4030b3fd2d3SAndreas Gohr                break;
4040b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_ENUMERATED:
4050b3fd2d3SAndreas Gohr                if ($isConstructed) {
4060b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded enumerated type is malformed.');
4070b3fd2d3SAndreas Gohr                }
4080b3fd2d3SAndreas Gohr                $type = EncodedType\EnumeratedType::withTag($tagNumber, $class, $this->decodeInteger($length));
4090b3fd2d3SAndreas Gohr                break;
4100b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_REAL:
4110b3fd2d3SAndreas Gohr                if ($isConstructed) {
4120b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded real type is malformed.');
4130b3fd2d3SAndreas Gohr                }
4140b3fd2d3SAndreas Gohr                $type = RealType::withTag($tagNumber, $class, $this->decodeReal($length));
4150b3fd2d3SAndreas Gohr                break;
4160b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_BIT_STRING:
4170b3fd2d3SAndreas Gohr                $type = EncodedType\BitStringType::withTag($tagNumber, $class, $isConstructed, $this->decodeBitString($length));
4180b3fd2d3SAndreas Gohr                break;
4190b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_OID:
4200b3fd2d3SAndreas Gohr                if ($isConstructed) {
4210b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded OID type is malformed.');
4220b3fd2d3SAndreas Gohr                }
4230b3fd2d3SAndreas Gohr                $type = OidType::withTag($tagNumber, $class, $this->decodeOid($length));
4240b3fd2d3SAndreas Gohr                break;
4250b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_RELATIVE_OID:
4260b3fd2d3SAndreas Gohr                if ($isConstructed) {
4270b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded relative OID type is malformed.');
4280b3fd2d3SAndreas Gohr                }
4290b3fd2d3SAndreas Gohr                $type = RelativeOidType::withTag($tagNumber, $class, $this->decodeRelativeOid($length));
4300b3fd2d3SAndreas Gohr                break;
4310b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_GENERALIZED_TIME:
4320b3fd2d3SAndreas Gohr                $type = EncodedType\GeneralizedTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeGeneralizedTime($length));
4330b3fd2d3SAndreas Gohr                break;
4340b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_UTC_TIME:
4350b3fd2d3SAndreas Gohr                $type = EncodedType\UtcTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeUtcTime($length));
4360b3fd2d3SAndreas Gohr                break;
4370b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_OCTET_STRING:
438*dad993c5SAndreas Gohr                $type = EncodedType\OctetStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4390b3fd2d3SAndreas Gohr                $this->pos += $length;
4400b3fd2d3SAndreas Gohr                break;
4410b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_GENERAL_STRING:
442*dad993c5SAndreas Gohr                $type = EncodedType\GeneralStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4430b3fd2d3SAndreas Gohr                $this->pos += $length;
4440b3fd2d3SAndreas Gohr                break;
4450b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_VISIBLE_STRING:
446*dad993c5SAndreas Gohr                $type = EncodedType\VisibleStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4470b3fd2d3SAndreas Gohr                $this->pos += $length;
4480b3fd2d3SAndreas Gohr                break;
4490b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_BMP_STRING:
450*dad993c5SAndreas Gohr                $type = EncodedType\BmpStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4510b3fd2d3SAndreas Gohr                $this->pos += $length;
4520b3fd2d3SAndreas Gohr                break;
4530b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_CHARACTER_STRING:
454*dad993c5SAndreas Gohr                $type = EncodedType\CharacterStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4550b3fd2d3SAndreas Gohr                $this->pos += $length;
4560b3fd2d3SAndreas Gohr                break;
4570b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_UNIVERSAL_STRING:
458*dad993c5SAndreas Gohr                $type = EncodedType\UniversalStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4590b3fd2d3SAndreas Gohr                $this->pos += $length;
4600b3fd2d3SAndreas Gohr                break;
4610b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_GRAPHIC_STRING:
462*dad993c5SAndreas Gohr                $type = EncodedType\GraphicStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4630b3fd2d3SAndreas Gohr                $this->pos += $length;
4640b3fd2d3SAndreas Gohr                break;
4650b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_VIDEOTEX_STRING:
466*dad993c5SAndreas Gohr                $type = EncodedType\VideotexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4670b3fd2d3SAndreas Gohr                $this->pos += $length;
4680b3fd2d3SAndreas Gohr                break;
4690b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_TELETEX_STRING:
470*dad993c5SAndreas Gohr                $type = EncodedType\TeletexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4710b3fd2d3SAndreas Gohr                $this->pos += $length;
4720b3fd2d3SAndreas Gohr                break;
4730b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_PRINTABLE_STRING:
474*dad993c5SAndreas Gohr                $type = EncodedType\PrintableStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4750b3fd2d3SAndreas Gohr                $this->pos += $length;
4760b3fd2d3SAndreas Gohr                break;
4770b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_NUMERIC_STRING:
478*dad993c5SAndreas Gohr                $type = EncodedType\NumericStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4790b3fd2d3SAndreas Gohr                $this->pos += $length;
4800b3fd2d3SAndreas Gohr                break;
4810b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_IA5_STRING:
482*dad993c5SAndreas Gohr                $type = EncodedType\IA5StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4830b3fd2d3SAndreas Gohr                $this->pos += $length;
4840b3fd2d3SAndreas Gohr                break;
4850b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_UTF8_STRING:
486*dad993c5SAndreas Gohr                $type = EncodedType\Utf8StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
4870b3fd2d3SAndreas Gohr                $this->pos += $length;
4880b3fd2d3SAndreas Gohr                break;
4890b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_SEQUENCE:
4900b3fd2d3SAndreas Gohr                if (!$isConstructed) {
4910b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded sequence type is malformed.');
4920b3fd2d3SAndreas Gohr                }
4930b3fd2d3SAndreas Gohr                $type = EncodedType\SequenceType::withTag($tagNumber, $class, $this->decodeConstructedType($length));
4940b3fd2d3SAndreas Gohr                break;
4950b3fd2d3SAndreas Gohr            case AbstractType::TAG_TYPE_SET:
4960b3fd2d3SAndreas Gohr                if (!$isConstructed) {
4970b3fd2d3SAndreas Gohr                    throw new EncoderException('The encoded set type is malformed.');
4980b3fd2d3SAndreas Gohr                }
4990b3fd2d3SAndreas Gohr                $type = EncodedType\SetType::withTag($tagNumber, $class, $this->decodeConstructedType($length));
5000b3fd2d3SAndreas Gohr                break;
5010b3fd2d3SAndreas Gohr            default:
5020b3fd2d3SAndreas Gohr                throw new EncoderException(sprintf('Unable to decode value to a type for tag %s.', $tagType));
5030b3fd2d3SAndreas Gohr        }
5040b3fd2d3SAndreas Gohr
5050b3fd2d3SAndreas Gohr        return $type;
5060b3fd2d3SAndreas Gohr    }
5070b3fd2d3SAndreas Gohr
5080b3fd2d3SAndreas Gohr    /**
5090b3fd2d3SAndreas Gohr     * @param int $length
5100b3fd2d3SAndreas Gohr     * @return int
5110b3fd2d3SAndreas Gohr     * @throws EncoderException
5120b3fd2d3SAndreas Gohr     * @throws PartialPduException
5130b3fd2d3SAndreas Gohr     */
5140b3fd2d3SAndreas Gohr    protected function decodeLongDefiniteLength(int $length): int
5150b3fd2d3SAndreas Gohr    {
5160b3fd2d3SAndreas Gohr        # The length of the length bytes is in the first 7 bits. So remove the MSB to get the value.
5170b3fd2d3SAndreas Gohr        $lengthOfLength = $length & ~0x80;
5180b3fd2d3SAndreas Gohr
5190b3fd2d3SAndreas Gohr        # The value of 127 is marked as reserved in the spec
5200b3fd2d3SAndreas Gohr        if ($lengthOfLength === 127) {
5210b3fd2d3SAndreas Gohr            throw new EncoderException('The decoded length cannot be equal to 127 bytes.');
5220b3fd2d3SAndreas Gohr        }
523fd0855ecSAndreas Gohr        if ($lengthOfLength > ($this->maxLen - $this->pos)) {
5240b3fd2d3SAndreas Gohr            throw new PartialPduException('Not enough data to decode the length.');
5250b3fd2d3SAndreas Gohr        }
5260b3fd2d3SAndreas Gohr        $endAt = $this->pos + $lengthOfLength;
5270b3fd2d3SAndreas Gohr
5280b3fd2d3SAndreas Gohr        # Base 256 encoded
5290b3fd2d3SAndreas Gohr        $length = 0;
5300b3fd2d3SAndreas Gohr        for ($this->pos; $this->pos < $endAt; $this->pos++) {
531*dad993c5SAndreas Gohr            $length = $length * 256 + ord($this->binary[$this->pos]);
5320b3fd2d3SAndreas Gohr        }
5330b3fd2d3SAndreas Gohr
5340b3fd2d3SAndreas Gohr        return $length;
5350b3fd2d3SAndreas Gohr    }
5360b3fd2d3SAndreas Gohr
5370b3fd2d3SAndreas Gohr    /**
5380b3fd2d3SAndreas Gohr     * Given what should be VLQ bytes represent an int, get the int and the length of bytes.
5390b3fd2d3SAndreas Gohr     *
5400b3fd2d3SAndreas Gohr     * @return string|int
5410b3fd2d3SAndreas Gohr     * @throws EncoderException
5420b3fd2d3SAndreas Gohr     */
5430b3fd2d3SAndreas Gohr    protected function getVlqBytesToInt()
5440b3fd2d3SAndreas Gohr    {
5450b3fd2d3SAndreas Gohr        $value = 0;
546fd0855ecSAndreas Gohr        $lshift = 0;
5470b3fd2d3SAndreas Gohr        $isBigInt = false;
5480b3fd2d3SAndreas Gohr
5490b3fd2d3SAndreas Gohr        for ($this->pos; $this->pos < $this->maxLen; $this->pos++) {
5500b3fd2d3SAndreas Gohr            if (!$isBigInt) {
5510b3fd2d3SAndreas Gohr                $lshift = $value << 7;
552fd0855ecSAndreas Gohr                # An overflow bitshift will result in a negative number or zero.
553fd0855ecSAndreas Gohr                # This will check if GMP is available and flip it to a bigint safe method in one shot.
554fd0855ecSAndreas Gohr                if ($value > 0 && $lshift <= 0) {
5550b3fd2d3SAndreas Gohr                    $isBigInt = true;
5560b3fd2d3SAndreas Gohr                    $this->throwIfBigIntGmpNeeded(true);
557*dad993c5SAndreas Gohr                    $value = gmp_init($value);
5580b3fd2d3SAndreas Gohr                }
5590b3fd2d3SAndreas Gohr            }
5600b3fd2d3SAndreas Gohr            if ($isBigInt) {
561*dad993c5SAndreas Gohr                $lshift = gmp_mul($value, gmp_pow('2', 7));
5620b3fd2d3SAndreas Gohr            }
563*dad993c5SAndreas Gohr            $orVal = (ord($this->binary[$this->pos]) & 0x7f);
5640b3fd2d3SAndreas Gohr            if ($isBigInt) {
565*dad993c5SAndreas Gohr                $value = gmp_or($lshift, gmp_init($orVal));
5660b3fd2d3SAndreas Gohr            } else {
5670b3fd2d3SAndreas Gohr                $value = $lshift | $orVal;
5680b3fd2d3SAndreas Gohr            }
5690b3fd2d3SAndreas Gohr            # We have reached the last byte if the MSB is not set.
570*dad993c5SAndreas Gohr            if ((ord($this->binary[$this->pos]) & 0x80) === 0) {
5710b3fd2d3SAndreas Gohr                $this->pos++;
572fd0855ecSAndreas Gohr
573*dad993c5SAndreas Gohr                return $isBigInt ? gmp_strval($value) : $value;
5740b3fd2d3SAndreas Gohr            }
5750b3fd2d3SAndreas Gohr        }
5760b3fd2d3SAndreas Gohr
5770b3fd2d3SAndreas Gohr        throw new EncoderException('Expected an ending byte to decode a VLQ, but none was found.');
5780b3fd2d3SAndreas Gohr    }
5790b3fd2d3SAndreas Gohr
5800b3fd2d3SAndreas Gohr    /**
5810b3fd2d3SAndreas Gohr     * Get the bytes that represent variable length quantity.
5820b3fd2d3SAndreas Gohr     *
5830b3fd2d3SAndreas Gohr     * @param string|int $int
5840b3fd2d3SAndreas Gohr     * @return string
5850b3fd2d3SAndreas Gohr     * @throws EncoderException
5860b3fd2d3SAndreas Gohr     */
5870b3fd2d3SAndreas Gohr    protected function intToVlqBytes($int)
5880b3fd2d3SAndreas Gohr    {
589*dad993c5SAndreas Gohr        $bigint = is_float($int + 0);
5900b3fd2d3SAndreas Gohr        $this->throwIfBigIntGmpNeeded($bigint);
5910b3fd2d3SAndreas Gohr
5920b3fd2d3SAndreas Gohr        if ($bigint) {
593*dad993c5SAndreas Gohr            $int = gmp_init($int);
594*dad993c5SAndreas Gohr            $bytes = chr(gmp_intval(gmp_and(gmp_init(0x7f), $int)));
595*dad993c5SAndreas Gohr            $int = gmp_div($int, gmp_pow(2, 7));
596*dad993c5SAndreas Gohr            $intVal = gmp_intval($int);
5970b3fd2d3SAndreas Gohr        } else {
598*dad993c5SAndreas Gohr            $bytes = chr(0x7f & $int);
5990b3fd2d3SAndreas Gohr            $int >>= 7;
6000b3fd2d3SAndreas Gohr            $intVal = $int;
6010b3fd2d3SAndreas Gohr        }
6020b3fd2d3SAndreas Gohr
6030b3fd2d3SAndreas Gohr        while ($intVal > 0) {
6040b3fd2d3SAndreas Gohr            if ($bigint) {
605*dad993c5SAndreas Gohr                $bytes = chr(gmp_intval(gmp_or(gmp_and(gmp_init(0x7f), $int), gmp_init(0x80)))) . $bytes;
606*dad993c5SAndreas Gohr                $int = gmp_div($int, gmp_pow('2', 7));
607*dad993c5SAndreas Gohr                $intVal = gmp_intval($int);
6080b3fd2d3SAndreas Gohr            } else {
609*dad993c5SAndreas Gohr                $bytes = chr((0x7f & $int) | 0x80) . $bytes;
6100b3fd2d3SAndreas Gohr                $int >>= 7;
6110b3fd2d3SAndreas Gohr                $intVal = $int;
6120b3fd2d3SAndreas Gohr            }
6130b3fd2d3SAndreas Gohr        }
6140b3fd2d3SAndreas Gohr
6150b3fd2d3SAndreas Gohr        return $bytes;
6160b3fd2d3SAndreas Gohr    }
6170b3fd2d3SAndreas Gohr
6180b3fd2d3SAndreas Gohr    /**
6190b3fd2d3SAndreas Gohr     * @param string|integer $integer
6200b3fd2d3SAndreas Gohr     * @throws EncoderException
6210b3fd2d3SAndreas Gohr     */
6220b3fd2d3SAndreas Gohr    protected function validateNumericInt($integer): void
6230b3fd2d3SAndreas Gohr    {
624*dad993c5SAndreas Gohr        if (is_int($integer)) {
6250b3fd2d3SAndreas Gohr            return;
6260b3fd2d3SAndreas Gohr        }
627*dad993c5SAndreas Gohr        if (is_string($integer) && is_numeric($integer) && strpos($integer, '.') === false) {
6280b3fd2d3SAndreas Gohr            return;
6290b3fd2d3SAndreas Gohr        }
6300b3fd2d3SAndreas Gohr
631fd0855ecSAndreas Gohr        throw new EncoderException(sprintf(
632fd0855ecSAndreas Gohr            'The value to encode for "%s" must be numeric.',
633fd0855ecSAndreas Gohr            $integer
634fd0855ecSAndreas Gohr        ));
6350b3fd2d3SAndreas Gohr    }
6360b3fd2d3SAndreas Gohr
6370b3fd2d3SAndreas Gohr    /**
6380b3fd2d3SAndreas Gohr     * @param int $num
6390b3fd2d3SAndreas Gohr     * @return string
6400b3fd2d3SAndreas Gohr     * @throws EncoderException
6410b3fd2d3SAndreas Gohr     */
6420b3fd2d3SAndreas Gohr    protected function encodeLongDefiniteLength(int $num)
6430b3fd2d3SAndreas Gohr    {
6440b3fd2d3SAndreas Gohr        $bytes = '';
6450b3fd2d3SAndreas Gohr        while ($num) {
646*dad993c5SAndreas Gohr            $bytes = (chr((int) ($num % 256))) . $bytes;
6470b3fd2d3SAndreas Gohr            $num = (int) ($num / 256);
6480b3fd2d3SAndreas Gohr        }
6490b3fd2d3SAndreas Gohr
650*dad993c5SAndreas Gohr        $length = strlen($bytes);
6510b3fd2d3SAndreas Gohr        if ($length >= 127) {
6520b3fd2d3SAndreas Gohr            throw new EncoderException('The encoded length cannot be greater than or equal to 127 bytes');
6530b3fd2d3SAndreas Gohr        }
6540b3fd2d3SAndreas Gohr
655*dad993c5SAndreas Gohr        return chr(0x80 | $length) . $bytes;
6560b3fd2d3SAndreas Gohr    }
6570b3fd2d3SAndreas Gohr
6580b3fd2d3SAndreas Gohr    /**
6590b3fd2d3SAndreas Gohr     * @param BitStringType $type
6600b3fd2d3SAndreas Gohr     * @return string
6610b3fd2d3SAndreas Gohr     */
6620b3fd2d3SAndreas Gohr    protected function encodeBitString(BitStringType $type)
6630b3fd2d3SAndreas Gohr    {
6640b3fd2d3SAndreas Gohr        $data = $type->getValue();
665*dad993c5SAndreas Gohr        $length = strlen($data);
6660b3fd2d3SAndreas Gohr        $unused = 0;
6670b3fd2d3SAndreas Gohr        if ($length % 8) {
6680b3fd2d3SAndreas Gohr            $unused = 8 - ($length % 8);
669*dad993c5SAndreas Gohr            $data = str_pad($data, $length + $unused, $this->options['bitstring_padding']);
670*dad993c5SAndreas Gohr            $length = strlen($data);
6710b3fd2d3SAndreas Gohr        }
6720b3fd2d3SAndreas Gohr
673*dad993c5SAndreas Gohr        $bytes = chr($unused);
6740b3fd2d3SAndreas Gohr        for ($i = 0; $i < $length / 8; $i++) {
675*dad993c5SAndreas Gohr            $bytes .= chr(bindec(substr($data, $i * 8, 8)));
6760b3fd2d3SAndreas Gohr        }
6770b3fd2d3SAndreas Gohr
6780b3fd2d3SAndreas Gohr        return $bytes;
6790b3fd2d3SAndreas Gohr    }
6800b3fd2d3SAndreas Gohr
6810b3fd2d3SAndreas Gohr    /**
6820b3fd2d3SAndreas Gohr     * @param RelativeOidType $type
6830b3fd2d3SAndreas Gohr     * @return string
6840b3fd2d3SAndreas Gohr     * @throws EncoderException
6850b3fd2d3SAndreas Gohr     */
6860b3fd2d3SAndreas Gohr    protected function encodeRelativeOid(RelativeOidType $type)
6870b3fd2d3SAndreas Gohr    {
688*dad993c5SAndreas Gohr        $oids = explode('.', $type->getValue());
6890b3fd2d3SAndreas Gohr
6900b3fd2d3SAndreas Gohr        $bytes = '';
6910b3fd2d3SAndreas Gohr        foreach ($oids as $oid) {
6920b3fd2d3SAndreas Gohr            $bytes .= $this->intToVlqBytes($oid);
6930b3fd2d3SAndreas Gohr        }
6940b3fd2d3SAndreas Gohr
6950b3fd2d3SAndreas Gohr        return $bytes;
6960b3fd2d3SAndreas Gohr    }
6970b3fd2d3SAndreas Gohr
6980b3fd2d3SAndreas Gohr    /**
6990b3fd2d3SAndreas Gohr     * @param OidType $type
7000b3fd2d3SAndreas Gohr     * @return string
7010b3fd2d3SAndreas Gohr     * @throws EncoderException
7020b3fd2d3SAndreas Gohr     */
7030b3fd2d3SAndreas Gohr    protected function encodeOid(OidType $type)
7040b3fd2d3SAndreas Gohr    {
705fd0855ecSAndreas Gohr        /** @var int[] $oids */
706*dad993c5SAndreas Gohr        $oids = explode('.', $type->getValue());
707*dad993c5SAndreas Gohr        $length = count($oids);
7080b3fd2d3SAndreas Gohr        if ($length < 2) {
709fd0855ecSAndreas Gohr            throw new EncoderException(sprintf(
710fd0855ecSAndreas Gohr                'To encode the OID it must have at least 2 components: %s',
711fd0855ecSAndreas Gohr                $type->getValue()
712fd0855ecSAndreas Gohr            ));
713fd0855ecSAndreas Gohr        }
714fd0855ecSAndreas Gohr        if ($oids[0] > 2) {
715fd0855ecSAndreas Gohr            throw new EncoderException(sprintf(
716fd0855ecSAndreas Gohr                'The value of the first OID component cannot be greater than 2. Received:  %s',
717fd0855ecSAndreas Gohr                $oids[0]
718fd0855ecSAndreas Gohr            ));
7190b3fd2d3SAndreas Gohr        }
7200b3fd2d3SAndreas Gohr
721fd0855ecSAndreas Gohr        # The first and second components of the OID are represented using the formula: (X * 40) + Y
722fd0855ecSAndreas Gohr        if ($oids[1] > self::MAX_SECOND_COMPONENT) {
723fd0855ecSAndreas Gohr            $this->throwIfBigIntGmpNeeded(true);
724*dad993c5SAndreas Gohr            $firstAndSecond = gmp_strval(gmp_add((string)($oids[0] * 40), $oids[1]));
725fd0855ecSAndreas Gohr        } else {
726fd0855ecSAndreas Gohr            $firstAndSecond = ($oids[0] * 40) + $oids[1];
727fd0855ecSAndreas Gohr        }
728fd0855ecSAndreas Gohr        $bytes = $this->intToVlqBytes($firstAndSecond);
7290b3fd2d3SAndreas Gohr
7300b3fd2d3SAndreas Gohr        for ($i = 2; $i < $length; $i++) {
7310b3fd2d3SAndreas Gohr            $bytes .= $this->intToVlqBytes($oids[$i]);
7320b3fd2d3SAndreas Gohr        }
7330b3fd2d3SAndreas Gohr
7340b3fd2d3SAndreas Gohr        return $bytes;
7350b3fd2d3SAndreas Gohr    }
7360b3fd2d3SAndreas Gohr
7370b3fd2d3SAndreas Gohr    /**
7380b3fd2d3SAndreas Gohr     * @param GeneralizedTimeType $type
7390b3fd2d3SAndreas Gohr     * @return string
7400b3fd2d3SAndreas Gohr     * @throws EncoderException
7410b3fd2d3SAndreas Gohr     */
7420b3fd2d3SAndreas Gohr    protected function encodeGeneralizedTime(GeneralizedTimeType $type)
7430b3fd2d3SAndreas Gohr    {
7440b3fd2d3SAndreas Gohr        return $this->encodeTime($type, 'YmdH');
7450b3fd2d3SAndreas Gohr    }
7460b3fd2d3SAndreas Gohr
7470b3fd2d3SAndreas Gohr    /**
7480b3fd2d3SAndreas Gohr     * @param UtcTimeType $type
7490b3fd2d3SAndreas Gohr     * @return string
7500b3fd2d3SAndreas Gohr     * @throws EncoderException
7510b3fd2d3SAndreas Gohr     */
7520b3fd2d3SAndreas Gohr    protected function encodeUtcTime(UtcTimeType $type)
7530b3fd2d3SAndreas Gohr    {
7540b3fd2d3SAndreas Gohr        return $this->encodeTime($type, 'ymdH');
7550b3fd2d3SAndreas Gohr    }
7560b3fd2d3SAndreas Gohr
7570b3fd2d3SAndreas Gohr    /**
7580b3fd2d3SAndreas Gohr     * @param AbstractTimeType $type
7590b3fd2d3SAndreas Gohr     * @param string $format
7600b3fd2d3SAndreas Gohr     * @return string
7610b3fd2d3SAndreas Gohr     * @throws EncoderException
7620b3fd2d3SAndreas Gohr     */
7630b3fd2d3SAndreas Gohr    protected function encodeTime(AbstractTimeType $type, string $format)
7640b3fd2d3SAndreas Gohr    {
7650b3fd2d3SAndreas Gohr        if ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_SECONDS || $type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_FRACTIONS) {
7660b3fd2d3SAndreas Gohr            $format .= 'is';
7670b3fd2d3SAndreas Gohr        } elseif ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_MINUTES) {
7680b3fd2d3SAndreas Gohr            $format .= 'i';
7690b3fd2d3SAndreas Gohr        }
7700b3fd2d3SAndreas Gohr
7710b3fd2d3SAndreas Gohr        # Is it possible to construct a datetime object in this way? Seems better to be safe with this check.
7720b3fd2d3SAndreas Gohr        if ($type->getValue()->format('H') === '24') {
7730b3fd2d3SAndreas Gohr            throw new EncoderException('Midnight must only be specified by 00, not 24.');
7740b3fd2d3SAndreas Gohr        }
7750b3fd2d3SAndreas Gohr
7760b3fd2d3SAndreas Gohr        return $this->formatDateTime(
7770b3fd2d3SAndreas Gohr            clone $type->getValue(),
7780b3fd2d3SAndreas Gohr            $type->getDateTimeFormat(),
7790b3fd2d3SAndreas Gohr            $type->getTimeZoneFormat(),
7800b3fd2d3SAndreas Gohr            $format
7810b3fd2d3SAndreas Gohr        );
7820b3fd2d3SAndreas Gohr    }
7830b3fd2d3SAndreas Gohr
7840b3fd2d3SAndreas Gohr    /**
7850b3fd2d3SAndreas Gohr     * @param \DateTime $dateTime
7860b3fd2d3SAndreas Gohr     * @param string $dateTimeFormat
7870b3fd2d3SAndreas Gohr     * @param string $tzFormat
7880b3fd2d3SAndreas Gohr     * @param string $format
7890b3fd2d3SAndreas Gohr     * @return string
7900b3fd2d3SAndreas Gohr     */
791*dad993c5SAndreas Gohr    protected function formatDateTime(DateTime $dateTime, string $dateTimeFormat, string $tzFormat, string $format)
7920b3fd2d3SAndreas Gohr    {
7930b3fd2d3SAndreas Gohr        if ($tzFormat === GeneralizedTimeType::TZ_LOCAL) {
794*dad993c5SAndreas Gohr            $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
7950b3fd2d3SAndreas Gohr        } elseif ($tzFormat === GeneralizedTimeType::TZ_UTC) {
796*dad993c5SAndreas Gohr            $dateTime->setTimezone(new DateTimeZone('UTC'));
7970b3fd2d3SAndreas Gohr        }
7980b3fd2d3SAndreas Gohr        $value = $dateTime->format($format);
7990b3fd2d3SAndreas Gohr
8000b3fd2d3SAndreas Gohr        # Fractions need special formatting, so we cannot directly include them in the format above.
8010b3fd2d3SAndreas Gohr        $ms = '';
8020b3fd2d3SAndreas Gohr        if ($dateTimeFormat === GeneralizedTimeType::FORMAT_FRACTIONS) {
803*dad993c5SAndreas Gohr            $ms = (string) rtrim($dateTime->format('u'), '0');
8040b3fd2d3SAndreas Gohr        }
8050b3fd2d3SAndreas Gohr
8060b3fd2d3SAndreas Gohr        $tz = '';
8070b3fd2d3SAndreas Gohr        if ($tzFormat === GeneralizedTimeType::TZ_UTC) {
8080b3fd2d3SAndreas Gohr            $tz = 'Z';
8090b3fd2d3SAndreas Gohr        } elseif ($tzFormat === GeneralizedTimeType::TZ_DIFF) {
8100b3fd2d3SAndreas Gohr            $tz = $dateTime->format('O');
8110b3fd2d3SAndreas Gohr        }
8120b3fd2d3SAndreas Gohr
8130b3fd2d3SAndreas Gohr        return $value . ($ms !== '' ? '.' . $ms : '') . $tz;
8140b3fd2d3SAndreas Gohr    }
8150b3fd2d3SAndreas Gohr
8160b3fd2d3SAndreas Gohr    /**
817fd0855ecSAndreas Gohr     * @param IntegerType|EnumeratedType $type
8180b3fd2d3SAndreas Gohr     * @return string
8190b3fd2d3SAndreas Gohr     * @throws EncoderException
8200b3fd2d3SAndreas Gohr     */
8210b3fd2d3SAndreas Gohr    protected function encodeInteger(AbstractType $type): string
8220b3fd2d3SAndreas Gohr    {
8230b3fd2d3SAndreas Gohr        $int = $type->getValue();
8240b3fd2d3SAndreas Gohr        $this->validateNumericInt($int);
8250b3fd2d3SAndreas Gohr        $isBigInt = $type->isBigInt();
8260b3fd2d3SAndreas Gohr        $isNegative = ($int < 0);
8270b3fd2d3SAndreas Gohr        $this->throwIfBigIntGmpNeeded($isBigInt);
8280b3fd2d3SAndreas Gohr        if ($isNegative) {
829*dad993c5SAndreas Gohr            $int = $isBigInt ? gmp_abs($type->getValue()) : ($int * -1);
8300b3fd2d3SAndreas Gohr        }
8310b3fd2d3SAndreas Gohr
8320b3fd2d3SAndreas Gohr        # Subtract one for Two's Complement...
8330b3fd2d3SAndreas Gohr        if ($isNegative) {
834*dad993c5SAndreas Gohr            $int = $isBigInt ? gmp_sub($int, '1') : $int - 1;
8350b3fd2d3SAndreas Gohr        }
8360b3fd2d3SAndreas Gohr
8370b3fd2d3SAndreas Gohr        if ($isBigInt) {
838*dad993c5SAndreas Gohr            $bytes = gmp_export($int);
8390b3fd2d3SAndreas Gohr        } else {
8400b3fd2d3SAndreas Gohr            # dechex can produce uneven hex while hex2bin requires it to be even
841*dad993c5SAndreas Gohr            $hex = dechex($int);
842*dad993c5SAndreas Gohr            $bytes = hex2bin((strlen($hex) % 2) === 0 ? $hex : '0' . $hex);
8430b3fd2d3SAndreas Gohr        }
8440b3fd2d3SAndreas Gohr
8450b3fd2d3SAndreas Gohr        # Two's Complement, invert the bits...
8460b3fd2d3SAndreas Gohr        if ($isNegative) {
847*dad993c5SAndreas Gohr            $len = strlen($bytes);
8480b3fd2d3SAndreas Gohr            for ($i = 0; $i < $len; $i++) {
849*dad993c5SAndreas Gohr                /** @var string $bytes */
8500b3fd2d3SAndreas Gohr                $bytes[$i] = ~$bytes[$i];
8510b3fd2d3SAndreas Gohr            }
8520b3fd2d3SAndreas Gohr        }
8530b3fd2d3SAndreas Gohr
8540b3fd2d3SAndreas Gohr        # MSB == Most Significant Bit. The one used for the sign.
855*dad993c5SAndreas Gohr        $msbSet = (bool) (ord($bytes[0]) & 0x80);
8560b3fd2d3SAndreas Gohr        if (!$isNegative && $msbSet) {
8570b3fd2d3SAndreas Gohr            $bytes = self::BOOL_FALSE . $bytes;
8580b3fd2d3SAndreas Gohr        } elseif ($isNegative && !$msbSet) {
8590b3fd2d3SAndreas Gohr            $bytes = self::BOOL_TRUE . $bytes;
8600b3fd2d3SAndreas Gohr        }
8610b3fd2d3SAndreas Gohr
8620b3fd2d3SAndreas Gohr        return $bytes;
8630b3fd2d3SAndreas Gohr    }
8640b3fd2d3SAndreas Gohr
8650b3fd2d3SAndreas Gohr    /**
8660b3fd2d3SAndreas Gohr     * @param RealType $type
8670b3fd2d3SAndreas Gohr     * @return string
8680b3fd2d3SAndreas Gohr     * @throws EncoderException
8690b3fd2d3SAndreas Gohr     */
8700b3fd2d3SAndreas Gohr    protected function encodeReal(RealType $type)
8710b3fd2d3SAndreas Gohr    {
8720b3fd2d3SAndreas Gohr        $real = $type->getValue();
8730b3fd2d3SAndreas Gohr
8740b3fd2d3SAndreas Gohr        # If the value is zero, the contents are omitted
8750b3fd2d3SAndreas Gohr        if ($real === ((float) 0)) {
8760b3fd2d3SAndreas Gohr            return '';
8770b3fd2d3SAndreas Gohr        }
8780b3fd2d3SAndreas Gohr        # If this is infinity, then a single octet of 0x40 is used.
8790b3fd2d3SAndreas Gohr        if ($real === INF) {
8800b3fd2d3SAndreas Gohr            return "\x40";
8810b3fd2d3SAndreas Gohr        }
8820b3fd2d3SAndreas Gohr        # If this is negative infinity, then a single octet of 0x41 is used.
8830b3fd2d3SAndreas Gohr        if ($real === -INF) {
8840b3fd2d3SAndreas Gohr            return "\x41";
8850b3fd2d3SAndreas Gohr        }
8860b3fd2d3SAndreas Gohr
8870b3fd2d3SAndreas Gohr        // @todo Real type encoding/decoding is rather complex. Need to implement this yet.
8880b3fd2d3SAndreas Gohr        throw new EncoderException('Real type encoding of this value not yet implemented.');
8890b3fd2d3SAndreas Gohr    }
8900b3fd2d3SAndreas Gohr
8910b3fd2d3SAndreas Gohr    /**
8920b3fd2d3SAndreas Gohr     * @param int $length
8930b3fd2d3SAndreas Gohr     * @return array
8940b3fd2d3SAndreas Gohr     * @throws EncoderException
8950b3fd2d3SAndreas Gohr     */
8960b3fd2d3SAndreas Gohr    protected function decodeGeneralizedTime($length): array
8970b3fd2d3SAndreas Gohr    {
8980b3fd2d3SAndreas Gohr        return $this->decodeTime('YmdH', GeneralizedTimeType::TIME_REGEX, GeneralizedTimeType::REGEX_MAP, $length);
8990b3fd2d3SAndreas Gohr    }
9000b3fd2d3SAndreas Gohr
9010b3fd2d3SAndreas Gohr    /**
9020b3fd2d3SAndreas Gohr     * @param int $length
9030b3fd2d3SAndreas Gohr     * @return array
9040b3fd2d3SAndreas Gohr     * @throws EncoderException
9050b3fd2d3SAndreas Gohr     */
9060b3fd2d3SAndreas Gohr    protected function decodeUtcTime($length): array
9070b3fd2d3SAndreas Gohr    {
9080b3fd2d3SAndreas Gohr        return $this->decodeTime('ymdH', UtcTimeType::TIME_REGEX, UtcTimeType::REGEX_MAP, $length);
9090b3fd2d3SAndreas Gohr    }
9100b3fd2d3SAndreas Gohr
9110b3fd2d3SAndreas Gohr    /**
9120b3fd2d3SAndreas Gohr     * @param string $format
9130b3fd2d3SAndreas Gohr     * @param string $regex
9140b3fd2d3SAndreas Gohr     * @param array $matchMap
9150b3fd2d3SAndreas Gohr     * @param int $length
9160b3fd2d3SAndreas Gohr     * @return array
9170b3fd2d3SAndreas Gohr     * @throws EncoderException
9180b3fd2d3SAndreas Gohr     */
9190b3fd2d3SAndreas Gohr    protected function decodeTime(string $format, string $regex, array $matchMap, $length): array
9200b3fd2d3SAndreas Gohr    {
921*dad993c5SAndreas Gohr        $bytes = substr($this->binary, $this->pos, $length);
9220b3fd2d3SAndreas Gohr        $this->pos += $length;
923*dad993c5SAndreas Gohr        if (!preg_match($regex, $bytes, $matches)) {
9240b3fd2d3SAndreas Gohr            throw new EncoderException('The datetime format is invalid and cannot be decoded.');
9250b3fd2d3SAndreas Gohr        }
9260b3fd2d3SAndreas Gohr        if ($matches[$matchMap['hours']] === '24') {
9270b3fd2d3SAndreas Gohr            throw new EncoderException('Midnight must only be specified by 00, but got 24.');
9280b3fd2d3SAndreas Gohr        }
9290b3fd2d3SAndreas Gohr        $tzFormat = AbstractTimeType::TZ_LOCAL;
9300b3fd2d3SAndreas Gohr        $dtFormat = AbstractTimeType::FORMAT_HOURS;
9310b3fd2d3SAndreas Gohr
9320b3fd2d3SAndreas Gohr        # Minutes
9330b3fd2d3SAndreas Gohr        if (isset($matches[$matchMap['minutes']]) && $matches[$matchMap['minutes']] !== '') {
9340b3fd2d3SAndreas Gohr            $dtFormat = AbstractTimeType::FORMAT_MINUTES;
9350b3fd2d3SAndreas Gohr            $format .= 'i';
9360b3fd2d3SAndreas Gohr        }
9370b3fd2d3SAndreas Gohr        # Seconds
9380b3fd2d3SAndreas Gohr        if (isset($matches[$matchMap['seconds']]) && $matches[$matchMap['seconds']] !== '') {
9390b3fd2d3SAndreas Gohr            $dtFormat = AbstractTimeType::FORMAT_SECONDS;
9400b3fd2d3SAndreas Gohr            $format .= 's';
9410b3fd2d3SAndreas Gohr        }
9420b3fd2d3SAndreas Gohr        # Fractions of a second
9430b3fd2d3SAndreas Gohr        if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') {
9440b3fd2d3SAndreas Gohr            $dtFormat = AbstractTimeType::FORMAT_FRACTIONS;
9450b3fd2d3SAndreas Gohr            $format .= '.u';
9460b3fd2d3SAndreas Gohr        }
9470b3fd2d3SAndreas Gohr        # Timezone
9480b3fd2d3SAndreas Gohr        if (isset($matches[$matchMap['timezone']]) && $matches[$matchMap['timezone']] !== '') {
9490b3fd2d3SAndreas Gohr            $tzFormat = $matches[$matchMap['timezone']] === 'Z' ? AbstractTimeType::TZ_UTC : AbstractTimeType::TZ_DIFF;
9500b3fd2d3SAndreas Gohr            $format .= 'T';
9510b3fd2d3SAndreas Gohr        }
9520b3fd2d3SAndreas Gohr        $this->validateDateFormat($matches, $matchMap);
9530b3fd2d3SAndreas Gohr
954*dad993c5SAndreas Gohr        $dateTime = DateTime::createFromFormat($format, $bytes);
9550b3fd2d3SAndreas Gohr        if ($dateTime === false) {
9560b3fd2d3SAndreas Gohr            throw new EncoderException('Unable to decode time to a DateTime object.');
9570b3fd2d3SAndreas Gohr        }
9580b3fd2d3SAndreas Gohr        $bytes = null;
9590b3fd2d3SAndreas Gohr
9600b3fd2d3SAndreas Gohr        return [$dateTime, $dtFormat, $tzFormat];
9610b3fd2d3SAndreas Gohr    }
9620b3fd2d3SAndreas Gohr
9630b3fd2d3SAndreas Gohr    /**
9640b3fd2d3SAndreas Gohr     * Some encodings have specific restrictions. Allow them to override and validate this.
9650b3fd2d3SAndreas Gohr     *
9660b3fd2d3SAndreas Gohr     * @param array $matches
9670b3fd2d3SAndreas Gohr     * @param array $matchMap
9680b3fd2d3SAndreas Gohr     */
9690b3fd2d3SAndreas Gohr    protected function validateDateFormat(array $matches, array $matchMap)
9700b3fd2d3SAndreas Gohr    {
9710b3fd2d3SAndreas Gohr    }
9720b3fd2d3SAndreas Gohr
9730b3fd2d3SAndreas Gohr    /**
974fd0855ecSAndreas Gohr     * @param int $length
9750b3fd2d3SAndreas Gohr     * @return string
9760b3fd2d3SAndreas Gohr     * @throws EncoderException
9770b3fd2d3SAndreas Gohr     */
9780b3fd2d3SAndreas Gohr    protected function decodeOid($length): string
9790b3fd2d3SAndreas Gohr    {
9800b3fd2d3SAndreas Gohr        if ($length === 0) {
9810b3fd2d3SAndreas Gohr            throw new EncoderException('Zero length not permitted for an OID type.');
9820b3fd2d3SAndreas Gohr        }
983fd0855ecSAndreas Gohr        # We need to get the first part here, as it's used to determine the first 2 components.
984fd0855ecSAndreas Gohr        $startedAt = $this->pos;
985fd0855ecSAndreas Gohr        $firstPart = $this->getVlqBytesToInt();
9860b3fd2d3SAndreas Gohr
987fd0855ecSAndreas Gohr        if ($firstPart < 80) {
988*dad993c5SAndreas Gohr            $oid = floor($firstPart / 40) . '.' . ($firstPart % 40);
989fd0855ecSAndreas Gohr        } else {
990fd0855ecSAndreas Gohr            $isBigInt = ($firstPart > PHP_INT_MAX);
991fd0855ecSAndreas Gohr            $this->throwIfBigIntGmpNeeded($isBigInt);
992fd0855ecSAndreas Gohr            # In this case, the first identifier is always 2.
993fd0855ecSAndreas Gohr            # But there is no limit on the value of the second identifier.
994*dad993c5SAndreas Gohr            $oid = '2.' . ($isBigInt ? gmp_strval(gmp_sub($firstPart, '80')) : (int)$firstPart - 80);
9950b3fd2d3SAndreas Gohr        }
9960b3fd2d3SAndreas Gohr
997fd0855ecSAndreas Gohr        # We could potentially have nothing left to decode at this point.
998fd0855ecSAndreas Gohr        $oidLength = $length - ($this->pos - $startedAt);
999fd0855ecSAndreas Gohr        $subIdentifiers = ($oidLength === 0) ? '' : '.' . $this->decodeRelativeOid($oidLength);
1000fd0855ecSAndreas Gohr
1001fd0855ecSAndreas Gohr        return $oid . $subIdentifiers;
10020b3fd2d3SAndreas Gohr    }
10030b3fd2d3SAndreas Gohr
10040b3fd2d3SAndreas Gohr    /**
1005fd0855ecSAndreas Gohr     * @param int $length
10060b3fd2d3SAndreas Gohr     * @return string
10070b3fd2d3SAndreas Gohr     * @throws EncoderException
10080b3fd2d3SAndreas Gohr     */
10090b3fd2d3SAndreas Gohr    protected function decodeRelativeOid($length): string
10100b3fd2d3SAndreas Gohr    {
10110b3fd2d3SAndreas Gohr        if ($length === 0) {
10120b3fd2d3SAndreas Gohr            throw new EncoderException('Zero length not permitted for an OID type.');
10130b3fd2d3SAndreas Gohr        }
10140b3fd2d3SAndreas Gohr        $oid = '';
10150b3fd2d3SAndreas Gohr        $endAt = $this->pos + $length;
10160b3fd2d3SAndreas Gohr
10170b3fd2d3SAndreas Gohr        while ($this->pos < $endAt) {
10180b3fd2d3SAndreas Gohr            $oid .= ($oid === '' ? '' : '.') . $this->getVlqBytesToInt();
10190b3fd2d3SAndreas Gohr        }
10200b3fd2d3SAndreas Gohr
10210b3fd2d3SAndreas Gohr        return $oid;
10220b3fd2d3SAndreas Gohr    }
10230b3fd2d3SAndreas Gohr
10240b3fd2d3SAndreas Gohr    /**
10250b3fd2d3SAndreas Gohr     * @return bool
10260b3fd2d3SAndreas Gohr     */
10270b3fd2d3SAndreas Gohr    protected function decodeBoolean(): bool
10280b3fd2d3SAndreas Gohr    {
10290b3fd2d3SAndreas Gohr        return ($this->binary[$this->pos++] !== self::BOOL_FALSE);
10300b3fd2d3SAndreas Gohr    }
10310b3fd2d3SAndreas Gohr
10320b3fd2d3SAndreas Gohr    /**
10330b3fd2d3SAndreas Gohr     * @param int $length
10340b3fd2d3SAndreas Gohr     * @return string
10350b3fd2d3SAndreas Gohr     * @throws EncoderException
10360b3fd2d3SAndreas Gohr     */
10370b3fd2d3SAndreas Gohr    protected function decodeBitString($length): string
10380b3fd2d3SAndreas Gohr    {
10390b3fd2d3SAndreas Gohr        # The first byte represents the number of unused bits at the end.
1040*dad993c5SAndreas Gohr        $unused = ord($this->binary[$this->pos++]);
10410b3fd2d3SAndreas Gohr
10420b3fd2d3SAndreas Gohr        if ($unused > 7) {
10430b3fd2d3SAndreas Gohr            throw new EncoderException(sprintf(
10440b3fd2d3SAndreas Gohr                'The unused bits in a bit string must be between 0 and 7, got: %s',
10450b3fd2d3SAndreas Gohr                $unused
10460b3fd2d3SAndreas Gohr            ));
10470b3fd2d3SAndreas Gohr        }
10480b3fd2d3SAndreas Gohr        if ($unused > 0 && $length < 1) {
10490b3fd2d3SAndreas Gohr            throw new EncoderException(sprintf(
10500b3fd2d3SAndreas Gohr                'If the bit string is empty the unused bits must be set to 0. However, it is set to %s with %s octets.',
10510b3fd2d3SAndreas Gohr                $unused,
10520b3fd2d3SAndreas Gohr                $length
10530b3fd2d3SAndreas Gohr            ));
10540b3fd2d3SAndreas Gohr        }
10550b3fd2d3SAndreas Gohr        $length--;
10560b3fd2d3SAndreas Gohr
10570b3fd2d3SAndreas Gohr        return $this->binaryToBitString($length, $unused);
10580b3fd2d3SAndreas Gohr    }
10590b3fd2d3SAndreas Gohr
10600b3fd2d3SAndreas Gohr    /**
10610b3fd2d3SAndreas Gohr     * @param int $length
10620b3fd2d3SAndreas Gohr     * @param int $unused
10630b3fd2d3SAndreas Gohr     * @return string
10640b3fd2d3SAndreas Gohr     */
10650b3fd2d3SAndreas Gohr    protected function binaryToBitString(int $length, int $unused): string
10660b3fd2d3SAndreas Gohr    {
10670b3fd2d3SAndreas Gohr        $bitstring = '';
10680b3fd2d3SAndreas Gohr        $endAt = $this->pos + $length;
10690b3fd2d3SAndreas Gohr
10700b3fd2d3SAndreas Gohr        for ($this->pos; $this->pos < $endAt; $this->pos++) {
1071*dad993c5SAndreas Gohr            $octet = sprintf("%08d", decbin(ord($this->binary[$this->pos])));
10720b3fd2d3SAndreas Gohr            if ($this->pos === ($endAt - 1) && $unused) {
1073*dad993c5SAndreas Gohr                $bitstring .= substr($octet, 0, ($unused * -1));
10740b3fd2d3SAndreas Gohr            } else {
10750b3fd2d3SAndreas Gohr                $bitstring .= $octet;
10760b3fd2d3SAndreas Gohr            }
10770b3fd2d3SAndreas Gohr        }
10780b3fd2d3SAndreas Gohr
10790b3fd2d3SAndreas Gohr        return $bitstring;
10800b3fd2d3SAndreas Gohr    }
10810b3fd2d3SAndreas Gohr
10820b3fd2d3SAndreas Gohr    /**
10830b3fd2d3SAndreas Gohr     * @param int $length
10840b3fd2d3SAndreas Gohr     * @return string|int number
10850b3fd2d3SAndreas Gohr     * @throws EncoderException
10860b3fd2d3SAndreas Gohr     */
10870b3fd2d3SAndreas Gohr    protected function decodeInteger($length)
10880b3fd2d3SAndreas Gohr    {
10890b3fd2d3SAndreas Gohr        if ($length === 0) {
10900b3fd2d3SAndreas Gohr            throw new EncoderException('Zero length not permitted for an integer type.');
10910b3fd2d3SAndreas Gohr        }
1092*dad993c5SAndreas Gohr        $bytes = substr($this->binary, $this->pos, $length);
10930b3fd2d3SAndreas Gohr        $this->pos += $length;
10940b3fd2d3SAndreas Gohr
1095*dad993c5SAndreas Gohr        $isNegative = (ord($bytes[0]) & 0x80);
10960b3fd2d3SAndreas Gohr        # Need to reverse Two's Complement. Invert the bits...
10970b3fd2d3SAndreas Gohr        if ($isNegative) {
10980b3fd2d3SAndreas Gohr            for ($i = 0; $i < $length; $i++) {
10990b3fd2d3SAndreas Gohr                $bytes[$i] = ~$bytes[$i];
11000b3fd2d3SAndreas Gohr            }
11010b3fd2d3SAndreas Gohr        }
1102*dad993c5SAndreas Gohr        $int = hexdec(bin2hex($bytes));
11030b3fd2d3SAndreas Gohr
1104*dad993c5SAndreas Gohr        $isBigInt = is_float($int);
11050b3fd2d3SAndreas Gohr        $this->throwIfBigIntGmpNeeded($isBigInt);
11060b3fd2d3SAndreas Gohr        if ($isBigInt) {
1107*dad993c5SAndreas Gohr            $int = gmp_import($bytes);
11080b3fd2d3SAndreas Gohr        }
11090b3fd2d3SAndreas Gohr        $bytes = null;
11100b3fd2d3SAndreas Gohr
11110b3fd2d3SAndreas Gohr        # Complete Two's Complement by adding 1 and turning it negative...
11120b3fd2d3SAndreas Gohr        if ($isNegative) {
1113*dad993c5SAndreas Gohr            $int = $isBigInt ? gmp_neg(gmp_add($int, '1')) : ($int + 1) * -1;
11140b3fd2d3SAndreas Gohr        }
11150b3fd2d3SAndreas Gohr
1116*dad993c5SAndreas Gohr        return $isBigInt ? gmp_strval($int) : $int;
11170b3fd2d3SAndreas Gohr    }
11180b3fd2d3SAndreas Gohr
11190b3fd2d3SAndreas Gohr    /**
11200b3fd2d3SAndreas Gohr     * @param bool $isBigInt
11210b3fd2d3SAndreas Gohr     * @throws EncoderException
11220b3fd2d3SAndreas Gohr     */
11230b3fd2d3SAndreas Gohr    protected function throwIfBigIntGmpNeeded(bool $isBigInt): void
11240b3fd2d3SAndreas Gohr    {
11250b3fd2d3SAndreas Gohr        if ($isBigInt && !$this->isGmpAvailable) {
11260b3fd2d3SAndreas Gohr            throw new EncoderException(sprintf(
11270b3fd2d3SAndreas Gohr                'An integer higher than PHP_INT_MAX int (%s) was encountered and the GMP extension is not loaded.',
11280b3fd2d3SAndreas Gohr                PHP_INT_MAX
11290b3fd2d3SAndreas Gohr            ));
11300b3fd2d3SAndreas Gohr        }
11310b3fd2d3SAndreas Gohr    }
11320b3fd2d3SAndreas Gohr
11330b3fd2d3SAndreas Gohr    /**
11340b3fd2d3SAndreas Gohr     * @param int $length
11350b3fd2d3SAndreas Gohr     * @return float
11360b3fd2d3SAndreas Gohr     * @throws EncoderException
11370b3fd2d3SAndreas Gohr     */
11380b3fd2d3SAndreas Gohr    protected function decodeReal($length): float
11390b3fd2d3SAndreas Gohr    {
11400b3fd2d3SAndreas Gohr        if ($length === 0) {
11410b3fd2d3SAndreas Gohr            return 0;
11420b3fd2d3SAndreas Gohr        }
11430b3fd2d3SAndreas Gohr
1144*dad993c5SAndreas Gohr        $ident = ord($this->binary[$this->pos++]);
11450b3fd2d3SAndreas Gohr        if ($ident === 0x40) {
11460b3fd2d3SAndreas Gohr            return INF;
11470b3fd2d3SAndreas Gohr        }
11480b3fd2d3SAndreas Gohr        if ($ident === 0x41) {
11490b3fd2d3SAndreas Gohr            return -INF;
11500b3fd2d3SAndreas Gohr        }
11510b3fd2d3SAndreas Gohr
11520b3fd2d3SAndreas Gohr        // @todo Real type encoding/decoding is rather complex. Need to implement this yet.
11530b3fd2d3SAndreas Gohr        throw new EncoderException('The real type encoding encountered is not yet implemented.');
11540b3fd2d3SAndreas Gohr    }
11550b3fd2d3SAndreas Gohr
11560b3fd2d3SAndreas Gohr    /**
11570b3fd2d3SAndreas Gohr     * Encoding subsets may require specific ordering on set types. Allow this to be overridden.
11580b3fd2d3SAndreas Gohr     *
11590b3fd2d3SAndreas Gohr     * @param SetType $set
11600b3fd2d3SAndreas Gohr     * @return string
11610b3fd2d3SAndreas Gohr     * @throws EncoderException
11620b3fd2d3SAndreas Gohr     */
11630b3fd2d3SAndreas Gohr    protected function encodeSet(SetType $set)
11640b3fd2d3SAndreas Gohr    {
11650b3fd2d3SAndreas Gohr        return $this->encodeConstructedType(...$set->getChildren());
11660b3fd2d3SAndreas Gohr    }
11670b3fd2d3SAndreas Gohr
11680b3fd2d3SAndreas Gohr    /**
11690b3fd2d3SAndreas Gohr     * Encoding subsets may require specific ordering on set of types. Allow this to be overridden.
11700b3fd2d3SAndreas Gohr     *
11710b3fd2d3SAndreas Gohr     * @param SetOfType $set
11720b3fd2d3SAndreas Gohr     * @return string
11730b3fd2d3SAndreas Gohr     * @throws EncoderException
11740b3fd2d3SAndreas Gohr     */
11750b3fd2d3SAndreas Gohr    protected function encodeSetOf(SetOfType $set)
11760b3fd2d3SAndreas Gohr    {
11770b3fd2d3SAndreas Gohr        return $this->encodeConstructedType(...$set->getChildren());
11780b3fd2d3SAndreas Gohr    }
11790b3fd2d3SAndreas Gohr
11800b3fd2d3SAndreas Gohr    /**
1181fd0855ecSAndreas Gohr     * @param AbstractType ...$types
11820b3fd2d3SAndreas Gohr     * @return string
11830b3fd2d3SAndreas Gohr     * @throws EncoderException
11840b3fd2d3SAndreas Gohr     */
11850b3fd2d3SAndreas Gohr    protected function encodeConstructedType(AbstractType ...$types)
11860b3fd2d3SAndreas Gohr    {
11870b3fd2d3SAndreas Gohr        $bytes = '';
11880b3fd2d3SAndreas Gohr
11890b3fd2d3SAndreas Gohr        foreach ($types as $type) {
11900b3fd2d3SAndreas Gohr            $bytes .= $this->encode($type);
11910b3fd2d3SAndreas Gohr        }
11920b3fd2d3SAndreas Gohr
11930b3fd2d3SAndreas Gohr        return $bytes;
11940b3fd2d3SAndreas Gohr    }
11950b3fd2d3SAndreas Gohr
11960b3fd2d3SAndreas Gohr    /**
11970b3fd2d3SAndreas Gohr     * @param int $length
11980b3fd2d3SAndreas Gohr     * @return array
11990b3fd2d3SAndreas Gohr     * @throws EncoderException
12000b3fd2d3SAndreas Gohr     * @throws PartialPduException
12010b3fd2d3SAndreas Gohr     */
12020b3fd2d3SAndreas Gohr    protected function decodeConstructedType($length)
12030b3fd2d3SAndreas Gohr    {
12040b3fd2d3SAndreas Gohr        $children = [];
12050b3fd2d3SAndreas Gohr        $endAt = $this->pos + $length;
12060b3fd2d3SAndreas Gohr
12070b3fd2d3SAndreas Gohr        while ($this->pos < $endAt) {
12080b3fd2d3SAndreas Gohr            $children[] = $this->decodeBytes();
12090b3fd2d3SAndreas Gohr        }
12100b3fd2d3SAndreas Gohr
12110b3fd2d3SAndreas Gohr        return $children;
12120b3fd2d3SAndreas Gohr    }
12130b3fd2d3SAndreas Gohr}
1214