1<?php
2/**
3 * This file is part of the FreeDSx ASN1 package.
4 *
5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace FreeDSx\Asn1\Encoder;
12
13use DateTime;
14use DateTimeZone;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Asn1\Exception\InvalidArgumentException;
17use FreeDSx\Asn1\Exception\PartialPduException;
18use FreeDSx\Asn1\Type\AbstractStringType;
19use FreeDSx\Asn1\Type\AbstractTimeType;
20use FreeDSx\Asn1\Type\AbstractType;
21use FreeDSx\Asn1\Type as EncodedType;
22use FreeDSx\Asn1\Type\BitStringType;
23use FreeDSx\Asn1\Type\BooleanType;
24use FreeDSx\Asn1\Type\EnumeratedType;
25use FreeDSx\Asn1\Type\GeneralizedTimeType;
26use FreeDSx\Asn1\Type\IncompleteType;
27use FreeDSx\Asn1\Type\IntegerType;
28use FreeDSx\Asn1\Type\NullType;
29use FreeDSx\Asn1\Type\OidType;
30use FreeDSx\Asn1\Type\RealType;
31use FreeDSx\Asn1\Type\RelativeOidType;
32use FreeDSx\Asn1\Type\SetOfType;
33use FreeDSx\Asn1\Type\SetType;
34use FreeDSx\Asn1\Type\UtcTimeType;
35use function bin2hex;
36use function bindec;
37use function chr;
38use function count;
39use function decbin;
40use function dechex;
41use function explode;
42use function extension_loaded;
43use function floor;
44use function gmp_abs;
45use function gmp_add;
46use function gmp_and;
47use function gmp_div;
48use function gmp_export;
49use function gmp_import;
50use function gmp_init;
51use function gmp_intval;
52use function gmp_mul;
53use function gmp_neg;
54use function gmp_or;
55use function gmp_pow;
56use function gmp_strval;
57use function gmp_sub;
58use function hex2bin;
59use function hexdec;
60use function is_float;
61use function is_int;
62use function is_numeric;
63use function is_string;
64use function ord;
65use function preg_match;
66use function rtrim;
67use function sprintf;
68use function str_pad;
69use function strlen;
70use function strpos;
71use function substr;
72
73/**
74 * Basic Encoding Rules (BER) encoder.
75 *
76 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
77 */
78class BerEncoder implements EncoderInterface
79{
80    /**
81     * Used to represent a bool false binary string.
82     */
83    protected const BOOL_FALSE = "\x00";
84
85    /**
86     * Used to represent a bool true binary string.
87     */
88    protected const BOOL_TRUE = "\xff";
89
90    /**
91     * Anything greater than this we assume we may need to deal with a bigint in an OIDs second component.
92     */
93    protected const MAX_SECOND_COMPONENT = PHP_INT_MAX - 80;
94
95    /**
96     * @var array
97     */
98    protected $tagMap = [
99        AbstractType::TAG_CLASS_APPLICATION => [],
100        AbstractType::TAG_CLASS_CONTEXT_SPECIFIC => [],
101        AbstractType::TAG_CLASS_PRIVATE => [],
102    ];
103
104    protected $tmpTagMap = [];
105
106    /**
107     * @var array
108     */
109    protected $options = [
110        'bitstring_padding' => '0',
111    ];
112
113    /**
114     * @var bool
115     */
116    protected $isGmpAvailable;
117
118    /**
119     * @var int
120     */
121    protected $pos;
122
123    /**
124     * @var int|null
125     */
126    protected $lastPos;
127
128    /**
129     * @var int
130     */
131    protected $maxLen;
132
133    /**
134     * @var string|null
135     */
136    protected $binary;
137
138    /**
139     * @param array $options
140     */
141    public function __construct(array $options = [])
142    {
143        $this->isGmpAvailable = extension_loaded('gmp');
144        $this->setOptions($options);
145    }
146
147    /**
148     * {@inheritdoc}
149     */
150    public function decode($binary, array $tagMap = []): AbstractType
151    {
152        $this->startEncoding($binary, $tagMap);
153        if ($this->maxLen === 0) {
154            throw new InvalidArgumentException('The data to decode cannot be empty.');
155        } elseif ($this->maxLen === 1) {
156            throw new PartialPduException('Received only 1 byte of data.');
157        }
158        $type = $this->decodeBytes(true);
159        $this->stopEncoding();
160
161        return $type;
162    }
163
164    /**
165     * {@inheritdoc}
166     */
167    public function complete(IncompleteType $type, int $tagType, array $tagMap = []): AbstractType
168    {
169        $lastPos = $this->lastPos;
170        $this->startEncoding($type->getValue(), $tagMap);
171        $newType = $this->decodeBytes(false, $tagType, $this->maxLen, $type->getIsConstructed(), AbstractType::TAG_CLASS_UNIVERSAL);
172        $this->stopEncoding();
173        $newType->setTagNumber($type->getTagNumber())
174            ->setTagClass($type->getTagClass());
175        $this->lastPos = $lastPos;
176
177        return $newType;
178    }
179
180    /**
181     * {@inheritdoc}
182     */
183    public function encode(AbstractType $type): string
184    {
185        switch ($type) {
186            case $type instanceof BooleanType:
187                $bytes = $type->getValue() ? self::BOOL_TRUE : self::BOOL_FALSE;
188                break;
189            case $type instanceof IntegerType:
190            case $type instanceof EnumeratedType:
191                $bytes = $this->encodeInteger($type);
192                break;
193            case $type instanceof RealType:
194                $bytes = $this->encodeReal($type);
195                break;
196            case $type instanceof AbstractStringType:
197                $bytes = $type->getValue();
198                break;
199            case $type instanceof SetOfType:
200                $bytes = $this->encodeSetOf($type);
201                break;
202            case $type instanceof SetType:
203                $bytes = $this->encodeSet($type);
204                break;
205            case $type->getIsConstructed():
206                $bytes = $this->encodeConstructedType(...$type->getChildren());
207                break;
208            case $type instanceof BitStringType:
209                $bytes = $this->encodeBitString($type);
210                break;
211            case $type instanceof OidType:
212                $bytes = $this->encodeOid($type);
213                break;
214            case $type instanceof RelativeOidType:
215                $bytes = $this->encodeRelativeOid($type);
216                break;
217            case $type instanceof GeneralizedTimeType:
218                $bytes = $this->encodeGeneralizedTime($type);
219                break;
220            case $type instanceof UtcTimeType:
221                $bytes = $this->encodeUtcTime($type);
222                break;
223            case $type instanceof NullType:
224                $bytes = '';
225                break;
226            default:
227                throw new EncoderException(sprintf(
228                    'The type "%s" is not currently supported.',
229                    get_class($type)
230                ));
231        }
232        $length = strlen($bytes);
233        $bytes = ($length < 128) ? chr($length) . $bytes : $this->encodeLongDefiniteLength($length) . $bytes;
234
235        # The first byte of a tag always contains the class (bits 8 and 7) and whether it is constructed (bit 6).
236        $tag = $type->getTagClass() | ($type->getIsConstructed() ? AbstractType::CONSTRUCTED_TYPE : 0);
237
238        $this->validateNumericInt($type->getTagNumber());
239        # For a high tag (>=31) we flip the first 5 bits on (0x1f) to make the first byte, then the subsequent bytes is
240        # the VLV encoding of the tag number.
241        if ($type->getTagNumber() >= 31) {
242            $bytes = chr($tag | 0x1f) . $this->intToVlqBytes($type->getTagNumber()) . $bytes;
243        # For a tag less than 31, everything fits comfortably into a single byte.
244        } else {
245            $bytes = chr($tag | $type->getTagNumber()) . $bytes;
246        }
247
248        return $bytes;
249    }
250
251    /**
252     * Map universal types to specific tag class values when decoding.
253     *
254     * @param int $class
255     * @param array $map
256     * @return $this
257     */
258    public function setTagMap(int $class, array $map)
259    {
260        if (isset($this->tagMap[$class])) {
261            $this->tagMap[$class] = $map;
262        }
263
264        return $this;
265    }
266
267    /**
268     * Get the options for the encoder.
269     *
270     * @return array
271     */
272    public function getOptions(): array
273    {
274        return $this->options;
275    }
276
277    /**
278     * Set the options for the encoder.
279     *
280     * @param array $options
281     * @return $this
282     */
283    public function setOptions(array $options)
284    {
285        if (isset($options['bitstring_padding']) && is_string($options['bitstring_padding'])) {
286            $this->options['bitstring_padding'] = $options['bitstring_padding'];
287        }
288
289        return $this;
290    }
291
292    /**
293     * @return int|null
294     */
295    public function getLastPosition(): ?int
296    {
297        return $this->lastPos;
298    }
299
300    protected function startEncoding(string $binary, array $tagMap): void
301    {
302        $this->tmpTagMap = $tagMap + $this->tagMap;
303        $this->binary = $binary;
304        $this->lastPos = null;
305        $this->pos = 0;
306        $this->maxLen = strlen($this->binary);
307    }
308
309    protected function stopEncoding(): void
310    {
311        $this->tmpTagMap = [];
312        $this->binary = null;
313        $this->maxLen = 0;
314        $this->lastPos = $this->pos;
315        $this->pos = 0;
316    }
317
318    /**
319     * @param bool $isRoot
320     * @param null|int $tagType
321     * @param null|int $length
322     * @param null|bool $isConstructed
323     * @param null|int $class
324     * @return AbstractType
325     * @throws EncoderException
326     * @throws PartialPduException
327     */
328    protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = null, $isConstructed = null, $class = null): AbstractType
329    {
330        $tagNumber = $tagType;
331        if ($tagType === null) {
332            $tag = ord($this->binary[$this->pos++]);
333            $class = $tag & 0xc0;
334            $isConstructed = (bool)($tag & AbstractType::CONSTRUCTED_TYPE);
335            $tagNumber = $tag & ~0xe0;
336
337            # Less than or equal to 30 is a low tag number represented in a single byte.
338            # A high tag number is determined using VLQ (like the OID identifier encoding) of the subsequent bytes.
339            if ($tagNumber > 30) {
340                try {
341                    $tagNumber = $this->getVlqBytesToInt();
342                    # It's possible we only got part of the VLQ for the high tag, as there is no way to know its ending length.
343                } catch (EncoderException $e) {
344                    if ($isRoot) {
345                        throw new PartialPduException(
346                            'Not enough data to decode the high tag number. No ending byte encountered for the VLQ bytes.'
347                        );
348                    }
349                    throw $e;
350                }
351            }
352
353            $length = ord($this->binary[$this->pos++]);
354            if ($length === 128) {
355                throw new EncoderException('Indefinite length encoding is not currently supported.');
356            }
357            if ($length > 128) {
358                $length = $this->decodeLongDefiniteLength($length);
359            }
360            $tagType = ($class === AbstractType::TAG_CLASS_UNIVERSAL) ? $tagNumber : ($this->tmpTagMap[$class][$tagNumber] ?? null);
361
362            if (($this->maxLen - $this->pos) < $length) {
363                $message = sprintf(
364                    'The expected byte length was %s, but received %s.',
365                    $length,
366                    ($this->maxLen - $this->pos)
367                );
368                if ($isRoot) {
369                    throw new PartialPduException($message);
370                } else {
371                    throw new EncoderException($message);
372                }
373            }
374
375            if ($tagType === null) {
376                $type = new IncompleteType(substr($this->binary, $this->pos, $length), $tagNumber, $class, $isConstructed);
377                $this->pos += $length;
378
379                return $type;
380            }
381        }
382
383        # Yes...this huge switch statement should be a separate method. However, it is faster inline when decoding
384        # lots of data (such as thousands of ASN.1 structures at a time).
385        switch ($tagType) {
386            case AbstractType::TAG_TYPE_BOOLEAN:
387                if ($length !== 1 || $isConstructed) {
388                    throw new EncoderException('The encoded boolean type is malformed.');
389                }
390                $type = EncodedType\BooleanType::withTag($tagNumber, $class, $this->decodeBoolean());
391                break;
392            case AbstractType::TAG_TYPE_NULL:
393                if ($length !== 0 || $isConstructed) {
394                    throw new EncoderException('The encoded null type is malformed.');
395                }
396                $type = EncodedType\NullType::withTag($tagNumber, $class);
397                break;
398            case AbstractType::TAG_TYPE_INTEGER:
399                if ($isConstructed) {
400                    throw new EncoderException('The encoded integer type is malformed.');
401                }
402                $type = EncodedType\IntegerType::withTag($tagNumber, $class, $this->decodeInteger($length));
403                break;
404            case AbstractType::TAG_TYPE_ENUMERATED:
405                if ($isConstructed) {
406                    throw new EncoderException('The encoded enumerated type is malformed.');
407                }
408                $type = EncodedType\EnumeratedType::withTag($tagNumber, $class, $this->decodeInteger($length));
409                break;
410            case AbstractType::TAG_TYPE_REAL:
411                if ($isConstructed) {
412                    throw new EncoderException('The encoded real type is malformed.');
413                }
414                $type = RealType::withTag($tagNumber, $class, $this->decodeReal($length));
415                break;
416            case AbstractType::TAG_TYPE_BIT_STRING:
417                $type = EncodedType\BitStringType::withTag($tagNumber, $class, $isConstructed, $this->decodeBitString($length));
418                break;
419            case AbstractType::TAG_TYPE_OID:
420                if ($isConstructed) {
421                    throw new EncoderException('The encoded OID type is malformed.');
422                }
423                $type = OidType::withTag($tagNumber, $class, $this->decodeOid($length));
424                break;
425            case AbstractType::TAG_TYPE_RELATIVE_OID:
426                if ($isConstructed) {
427                    throw new EncoderException('The encoded relative OID type is malformed.');
428                }
429                $type = RelativeOidType::withTag($tagNumber, $class, $this->decodeRelativeOid($length));
430                break;
431            case AbstractType::TAG_TYPE_GENERALIZED_TIME:
432                $type = EncodedType\GeneralizedTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeGeneralizedTime($length));
433                break;
434            case AbstractType::TAG_TYPE_UTC_TIME:
435                $type = EncodedType\UtcTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeUtcTime($length));
436                break;
437            case AbstractType::TAG_TYPE_OCTET_STRING:
438                $type = EncodedType\OctetStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
439                $this->pos += $length;
440                break;
441            case AbstractType::TAG_TYPE_GENERAL_STRING:
442                $type = EncodedType\GeneralStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
443                $this->pos += $length;
444                break;
445            case AbstractType::TAG_TYPE_VISIBLE_STRING:
446                $type = EncodedType\VisibleStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
447                $this->pos += $length;
448                break;
449            case AbstractType::TAG_TYPE_BMP_STRING:
450                $type = EncodedType\BmpStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
451                $this->pos += $length;
452                break;
453            case AbstractType::TAG_TYPE_CHARACTER_STRING:
454                $type = EncodedType\CharacterStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
455                $this->pos += $length;
456                break;
457            case AbstractType::TAG_TYPE_UNIVERSAL_STRING:
458                $type = EncodedType\UniversalStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
459                $this->pos += $length;
460                break;
461            case AbstractType::TAG_TYPE_GRAPHIC_STRING:
462                $type = EncodedType\GraphicStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
463                $this->pos += $length;
464                break;
465            case AbstractType::TAG_TYPE_VIDEOTEX_STRING:
466                $type = EncodedType\VideotexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
467                $this->pos += $length;
468                break;
469            case AbstractType::TAG_TYPE_TELETEX_STRING:
470                $type = EncodedType\TeletexStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
471                $this->pos += $length;
472                break;
473            case AbstractType::TAG_TYPE_PRINTABLE_STRING:
474                $type = EncodedType\PrintableStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
475                $this->pos += $length;
476                break;
477            case AbstractType::TAG_TYPE_NUMERIC_STRING:
478                $type = EncodedType\NumericStringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
479                $this->pos += $length;
480                break;
481            case AbstractType::TAG_TYPE_IA5_STRING:
482                $type = EncodedType\IA5StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
483                $this->pos += $length;
484                break;
485            case AbstractType::TAG_TYPE_UTF8_STRING:
486                $type = EncodedType\Utf8StringType::withTag($tagNumber, $class, $isConstructed, substr($this->binary, $this->pos, $length));
487                $this->pos += $length;
488                break;
489            case AbstractType::TAG_TYPE_SEQUENCE:
490                if (!$isConstructed) {
491                    throw new EncoderException('The encoded sequence type is malformed.');
492                }
493                $type = EncodedType\SequenceType::withTag($tagNumber, $class, $this->decodeConstructedType($length));
494                break;
495            case AbstractType::TAG_TYPE_SET:
496                if (!$isConstructed) {
497                    throw new EncoderException('The encoded set type is malformed.');
498                }
499                $type = EncodedType\SetType::withTag($tagNumber, $class, $this->decodeConstructedType($length));
500                break;
501            default:
502                throw new EncoderException(sprintf('Unable to decode value to a type for tag %s.', $tagType));
503        }
504
505        return $type;
506    }
507
508    /**
509     * @param int $length
510     * @return int
511     * @throws EncoderException
512     * @throws PartialPduException
513     */
514    protected function decodeLongDefiniteLength(int $length): int
515    {
516        # The length of the length bytes is in the first 7 bits. So remove the MSB to get the value.
517        $lengthOfLength = $length & ~0x80;
518
519        # The value of 127 is marked as reserved in the spec
520        if ($lengthOfLength === 127) {
521            throw new EncoderException('The decoded length cannot be equal to 127 bytes.');
522        }
523        if ($lengthOfLength > ($this->maxLen - $this->pos)) {
524            throw new PartialPduException('Not enough data to decode the length.');
525        }
526        $endAt = $this->pos + $lengthOfLength;
527
528        # Base 256 encoded
529        $length = 0;
530        for ($this->pos; $this->pos < $endAt; $this->pos++) {
531            $length = $length * 256 + ord($this->binary[$this->pos]);
532        }
533
534        return $length;
535    }
536
537    /**
538     * Given what should be VLQ bytes represent an int, get the int and the length of bytes.
539     *
540     * @return string|int
541     * @throws EncoderException
542     */
543    protected function getVlqBytesToInt()
544    {
545        $value = 0;
546        $lshift = 0;
547        $isBigInt = false;
548
549        for ($this->pos; $this->pos < $this->maxLen; $this->pos++) {
550            if (!$isBigInt) {
551                $lshift = $value << 7;
552                # An overflow bitshift will result in a negative number or zero.
553                # This will check if GMP is available and flip it to a bigint safe method in one shot.
554                if ($value > 0 && $lshift <= 0) {
555                    $isBigInt = true;
556                    $this->throwIfBigIntGmpNeeded(true);
557                    $value = gmp_init($value);
558                }
559            }
560            if ($isBigInt) {
561                $lshift = gmp_mul($value, gmp_pow('2', 7));
562            }
563            $orVal = (ord($this->binary[$this->pos]) & 0x7f);
564            if ($isBigInt) {
565                $value = gmp_or($lshift, gmp_init($orVal));
566            } else {
567                $value = $lshift | $orVal;
568            }
569            # We have reached the last byte if the MSB is not set.
570            if ((ord($this->binary[$this->pos]) & 0x80) === 0) {
571                $this->pos++;
572
573                return $isBigInt ? gmp_strval($value) : $value;
574            }
575        }
576
577        throw new EncoderException('Expected an ending byte to decode a VLQ, but none was found.');
578    }
579
580    /**
581     * Get the bytes that represent variable length quantity.
582     *
583     * @param string|int $int
584     * @return string
585     * @throws EncoderException
586     */
587    protected function intToVlqBytes($int)
588    {
589        $bigint = is_float($int + 0);
590        $this->throwIfBigIntGmpNeeded($bigint);
591
592        if ($bigint) {
593            $int = gmp_init($int);
594            $bytes = chr(gmp_intval(gmp_and(gmp_init(0x7f), $int)));
595            $int = gmp_div($int, gmp_pow(2, 7));
596            $intVal = gmp_intval($int);
597        } else {
598            $bytes = chr(0x7f & $int);
599            $int >>= 7;
600            $intVal = $int;
601        }
602
603        while ($intVal > 0) {
604            if ($bigint) {
605                $bytes = chr(gmp_intval(gmp_or(gmp_and(gmp_init(0x7f), $int), gmp_init(0x80)))) . $bytes;
606                $int = gmp_div($int, gmp_pow('2', 7));
607                $intVal = gmp_intval($int);
608            } else {
609                $bytes = chr((0x7f & $int) | 0x80) . $bytes;
610                $int >>= 7;
611                $intVal = $int;
612            }
613        }
614
615        return $bytes;
616    }
617
618    /**
619     * @param string|integer $integer
620     * @throws EncoderException
621     */
622    protected function validateNumericInt($integer): void
623    {
624        if (is_int($integer)) {
625            return;
626        }
627        if (is_string($integer) && is_numeric($integer) && strpos($integer, '.') === false) {
628            return;
629        }
630
631        throw new EncoderException(sprintf(
632            'The value to encode for "%s" must be numeric.',
633            $integer
634        ));
635    }
636
637    /**
638     * @param int $num
639     * @return string
640     * @throws EncoderException
641     */
642    protected function encodeLongDefiniteLength(int $num)
643    {
644        $bytes = '';
645        while ($num) {
646            $bytes = (chr((int) ($num % 256))) . $bytes;
647            $num = (int) ($num / 256);
648        }
649
650        $length = strlen($bytes);
651        if ($length >= 127) {
652            throw new EncoderException('The encoded length cannot be greater than or equal to 127 bytes');
653        }
654
655        return chr(0x80 | $length) . $bytes;
656    }
657
658    /**
659     * @param BitStringType $type
660     * @return string
661     */
662    protected function encodeBitString(BitStringType $type)
663    {
664        $data = $type->getValue();
665        $length = strlen($data);
666        $unused = 0;
667        if ($length % 8) {
668            $unused = 8 - ($length % 8);
669            $data = str_pad($data, $length + $unused, $this->options['bitstring_padding']);
670            $length = strlen($data);
671        }
672
673        $bytes = chr($unused);
674        for ($i = 0; $i < $length / 8; $i++) {
675            $bytes .= chr(bindec(substr($data, $i * 8, 8)));
676        }
677
678        return $bytes;
679    }
680
681    /**
682     * @param RelativeOidType $type
683     * @return string
684     * @throws EncoderException
685     */
686    protected function encodeRelativeOid(RelativeOidType $type)
687    {
688        $oids = explode('.', $type->getValue());
689
690        $bytes = '';
691        foreach ($oids as $oid) {
692            $bytes .= $this->intToVlqBytes($oid);
693        }
694
695        return $bytes;
696    }
697
698    /**
699     * @param OidType $type
700     * @return string
701     * @throws EncoderException
702     */
703    protected function encodeOid(OidType $type)
704    {
705        /** @var int[] $oids */
706        $oids = explode('.', $type->getValue());
707        $length = count($oids);
708        if ($length < 2) {
709            throw new EncoderException(sprintf(
710                'To encode the OID it must have at least 2 components: %s',
711                $type->getValue()
712            ));
713        }
714        if ($oids[0] > 2) {
715            throw new EncoderException(sprintf(
716                'The value of the first OID component cannot be greater than 2. Received:  %s',
717                $oids[0]
718            ));
719        }
720
721        # The first and second components of the OID are represented using the formula: (X * 40) + Y
722        if ($oids[1] > self::MAX_SECOND_COMPONENT) {
723            $this->throwIfBigIntGmpNeeded(true);
724            $firstAndSecond = gmp_strval(gmp_add((string)($oids[0] * 40), $oids[1]));
725        } else {
726            $firstAndSecond = ($oids[0] * 40) + $oids[1];
727        }
728        $bytes = $this->intToVlqBytes($firstAndSecond);
729
730        for ($i = 2; $i < $length; $i++) {
731            $bytes .= $this->intToVlqBytes($oids[$i]);
732        }
733
734        return $bytes;
735    }
736
737    /**
738     * @param GeneralizedTimeType $type
739     * @return string
740     * @throws EncoderException
741     */
742    protected function encodeGeneralizedTime(GeneralizedTimeType $type)
743    {
744        return $this->encodeTime($type, 'YmdH');
745    }
746
747    /**
748     * @param UtcTimeType $type
749     * @return string
750     * @throws EncoderException
751     */
752    protected function encodeUtcTime(UtcTimeType $type)
753    {
754        return $this->encodeTime($type, 'ymdH');
755    }
756
757    /**
758     * @param AbstractTimeType $type
759     * @param string $format
760     * @return string
761     * @throws EncoderException
762     */
763    protected function encodeTime(AbstractTimeType $type, string $format)
764    {
765        if ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_SECONDS || $type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_FRACTIONS) {
766            $format .= 'is';
767        } elseif ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_MINUTES) {
768            $format .= 'i';
769        }
770
771        # Is it possible to construct a datetime object in this way? Seems better to be safe with this check.
772        if ($type->getValue()->format('H') === '24') {
773            throw new EncoderException('Midnight must only be specified by 00, not 24.');
774        }
775
776        return $this->formatDateTime(
777            clone $type->getValue(),
778            $type->getDateTimeFormat(),
779            $type->getTimeZoneFormat(),
780            $format
781        );
782    }
783
784    /**
785     * @param \DateTime $dateTime
786     * @param string $dateTimeFormat
787     * @param string $tzFormat
788     * @param string $format
789     * @return string
790     */
791    protected function formatDateTime(DateTime $dateTime, string $dateTimeFormat, string $tzFormat, string $format)
792    {
793        if ($tzFormat === GeneralizedTimeType::TZ_LOCAL) {
794            $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
795        } elseif ($tzFormat === GeneralizedTimeType::TZ_UTC) {
796            $dateTime->setTimezone(new DateTimeZone('UTC'));
797        }
798        $value = $dateTime->format($format);
799
800        # Fractions need special formatting, so we cannot directly include them in the format above.
801        $ms = '';
802        if ($dateTimeFormat === GeneralizedTimeType::FORMAT_FRACTIONS) {
803            $ms = (string) rtrim($dateTime->format('u'), '0');
804        }
805
806        $tz = '';
807        if ($tzFormat === GeneralizedTimeType::TZ_UTC) {
808            $tz = 'Z';
809        } elseif ($tzFormat === GeneralizedTimeType::TZ_DIFF) {
810            $tz = $dateTime->format('O');
811        }
812
813        return $value . ($ms !== '' ? '.' . $ms : '') . $tz;
814    }
815
816    /**
817     * @param IntegerType|EnumeratedType $type
818     * @return string
819     * @throws EncoderException
820     */
821    protected function encodeInteger(AbstractType $type): string
822    {
823        $int = $type->getValue();
824        $this->validateNumericInt($int);
825        $isBigInt = $type->isBigInt();
826        $isNegative = ($int < 0);
827        $this->throwIfBigIntGmpNeeded($isBigInt);
828        if ($isNegative) {
829            $int = $isBigInt ? gmp_abs($type->getValue()) : ($int * -1);
830        }
831
832        # Subtract one for Two's Complement...
833        if ($isNegative) {
834            $int = $isBigInt ? gmp_sub($int, '1') : $int - 1;
835        }
836
837        if ($isBigInt) {
838            $bytes = gmp_export($int);
839        } else {
840            # dechex can produce uneven hex while hex2bin requires it to be even
841            $hex = dechex($int);
842            $bytes = hex2bin((strlen($hex) % 2) === 0 ? $hex : '0' . $hex);
843        }
844
845        # Two's Complement, invert the bits...
846        if ($isNegative) {
847            $len = strlen($bytes);
848            for ($i = 0; $i < $len; $i++) {
849                /** @var string $bytes */
850                $bytes[$i] = ~$bytes[$i];
851            }
852        }
853
854        # MSB == Most Significant Bit. The one used for the sign.
855        $msbSet = (bool) (ord($bytes[0]) & 0x80);
856        if (!$isNegative && $msbSet) {
857            $bytes = self::BOOL_FALSE . $bytes;
858        } elseif ($isNegative && !$msbSet) {
859            $bytes = self::BOOL_TRUE . $bytes;
860        }
861
862        return $bytes;
863    }
864
865    /**
866     * @param RealType $type
867     * @return string
868     * @throws EncoderException
869     */
870    protected function encodeReal(RealType $type)
871    {
872        $real = $type->getValue();
873
874        # If the value is zero, the contents are omitted
875        if ($real === ((float) 0)) {
876            return '';
877        }
878        # If this is infinity, then a single octet of 0x40 is used.
879        if ($real === INF) {
880            return "\x40";
881        }
882        # If this is negative infinity, then a single octet of 0x41 is used.
883        if ($real === -INF) {
884            return "\x41";
885        }
886
887        // @todo Real type encoding/decoding is rather complex. Need to implement this yet.
888        throw new EncoderException('Real type encoding of this value not yet implemented.');
889    }
890
891    /**
892     * @param int $length
893     * @return array
894     * @throws EncoderException
895     */
896    protected function decodeGeneralizedTime($length): array
897    {
898        return $this->decodeTime('YmdH', GeneralizedTimeType::TIME_REGEX, GeneralizedTimeType::REGEX_MAP, $length);
899    }
900
901    /**
902     * @param int $length
903     * @return array
904     * @throws EncoderException
905     */
906    protected function decodeUtcTime($length): array
907    {
908        return $this->decodeTime('ymdH', UtcTimeType::TIME_REGEX, UtcTimeType::REGEX_MAP, $length);
909    }
910
911    /**
912     * @param string $format
913     * @param string $regex
914     * @param array $matchMap
915     * @param int $length
916     * @return array
917     * @throws EncoderException
918     */
919    protected function decodeTime(string $format, string $regex, array $matchMap, $length): array
920    {
921        $bytes = substr($this->binary, $this->pos, $length);
922        $this->pos += $length;
923        if (!preg_match($regex, $bytes, $matches)) {
924            throw new EncoderException('The datetime format is invalid and cannot be decoded.');
925        }
926        if ($matches[$matchMap['hours']] === '24') {
927            throw new EncoderException('Midnight must only be specified by 00, but got 24.');
928        }
929        $tzFormat = AbstractTimeType::TZ_LOCAL;
930        $dtFormat = AbstractTimeType::FORMAT_HOURS;
931
932        # Minutes
933        if (isset($matches[$matchMap['minutes']]) && $matches[$matchMap['minutes']] !== '') {
934            $dtFormat = AbstractTimeType::FORMAT_MINUTES;
935            $format .= 'i';
936        }
937        # Seconds
938        if (isset($matches[$matchMap['seconds']]) && $matches[$matchMap['seconds']] !== '') {
939            $dtFormat = AbstractTimeType::FORMAT_SECONDS;
940            $format .= 's';
941        }
942        # Fractions of a second
943        if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') {
944            $dtFormat = AbstractTimeType::FORMAT_FRACTIONS;
945            $format .= '.u';
946        }
947        # Timezone
948        if (isset($matches[$matchMap['timezone']]) && $matches[$matchMap['timezone']] !== '') {
949            $tzFormat = $matches[$matchMap['timezone']] === 'Z' ? AbstractTimeType::TZ_UTC : AbstractTimeType::TZ_DIFF;
950            $format .= 'T';
951        }
952        $this->validateDateFormat($matches, $matchMap);
953
954        $dateTime = DateTime::createFromFormat($format, $bytes);
955        if ($dateTime === false) {
956            throw new EncoderException('Unable to decode time to a DateTime object.');
957        }
958        $bytes = null;
959
960        return [$dateTime, $dtFormat, $tzFormat];
961    }
962
963    /**
964     * Some encodings have specific restrictions. Allow them to override and validate this.
965     *
966     * @param array $matches
967     * @param array $matchMap
968     */
969    protected function validateDateFormat(array $matches, array $matchMap)
970    {
971    }
972
973    /**
974     * @param int $length
975     * @return string
976     * @throws EncoderException
977     */
978    protected function decodeOid($length): string
979    {
980        if ($length === 0) {
981            throw new EncoderException('Zero length not permitted for an OID type.');
982        }
983        # We need to get the first part here, as it's used to determine the first 2 components.
984        $startedAt = $this->pos;
985        $firstPart = $this->getVlqBytesToInt();
986
987        if ($firstPart < 80) {
988            $oid = floor($firstPart / 40) . '.' . ($firstPart % 40);
989        } else {
990            $isBigInt = ($firstPart > PHP_INT_MAX);
991            $this->throwIfBigIntGmpNeeded($isBigInt);
992            # In this case, the first identifier is always 2.
993            # But there is no limit on the value of the second identifier.
994            $oid = '2.' . ($isBigInt ? gmp_strval(gmp_sub($firstPart, '80')) : (int)$firstPart - 80);
995        }
996
997        # We could potentially have nothing left to decode at this point.
998        $oidLength = $length - ($this->pos - $startedAt);
999        $subIdentifiers = ($oidLength === 0) ? '' : '.' . $this->decodeRelativeOid($oidLength);
1000
1001        return $oid . $subIdentifiers;
1002    }
1003
1004    /**
1005     * @param int $length
1006     * @return string
1007     * @throws EncoderException
1008     */
1009    protected function decodeRelativeOid($length): string
1010    {
1011        if ($length === 0) {
1012            throw new EncoderException('Zero length not permitted for an OID type.');
1013        }
1014        $oid = '';
1015        $endAt = $this->pos + $length;
1016
1017        while ($this->pos < $endAt) {
1018            $oid .= ($oid === '' ? '' : '.') . $this->getVlqBytesToInt();
1019        }
1020
1021        return $oid;
1022    }
1023
1024    /**
1025     * @return bool
1026     */
1027    protected function decodeBoolean(): bool
1028    {
1029        return ($this->binary[$this->pos++] !== self::BOOL_FALSE);
1030    }
1031
1032    /**
1033     * @param int $length
1034     * @return string
1035     * @throws EncoderException
1036     */
1037    protected function decodeBitString($length): string
1038    {
1039        # The first byte represents the number of unused bits at the end.
1040        $unused = ord($this->binary[$this->pos++]);
1041
1042        if ($unused > 7) {
1043            throw new EncoderException(sprintf(
1044                'The unused bits in a bit string must be between 0 and 7, got: %s',
1045                $unused
1046            ));
1047        }
1048        if ($unused > 0 && $length < 1) {
1049            throw new EncoderException(sprintf(
1050                'If the bit string is empty the unused bits must be set to 0. However, it is set to %s with %s octets.',
1051                $unused,
1052                $length
1053            ));
1054        }
1055        $length--;
1056
1057        return $this->binaryToBitString($length, $unused);
1058    }
1059
1060    /**
1061     * @param int $length
1062     * @param int $unused
1063     * @return string
1064     */
1065    protected function binaryToBitString(int $length, int $unused): string
1066    {
1067        $bitstring = '';
1068        $endAt = $this->pos + $length;
1069
1070        for ($this->pos; $this->pos < $endAt; $this->pos++) {
1071            $octet = sprintf("%08d", decbin(ord($this->binary[$this->pos])));
1072            if ($this->pos === ($endAt - 1) && $unused) {
1073                $bitstring .= substr($octet, 0, ($unused * -1));
1074            } else {
1075                $bitstring .= $octet;
1076            }
1077        }
1078
1079        return $bitstring;
1080    }
1081
1082    /**
1083     * @param int $length
1084     * @return string|int number
1085     * @throws EncoderException
1086     */
1087    protected function decodeInteger($length)
1088    {
1089        if ($length === 0) {
1090            throw new EncoderException('Zero length not permitted for an integer type.');
1091        }
1092        $bytes = substr($this->binary, $this->pos, $length);
1093        $this->pos += $length;
1094
1095        $isNegative = (ord($bytes[0]) & 0x80);
1096        # Need to reverse Two's Complement. Invert the bits...
1097        if ($isNegative) {
1098            for ($i = 0; $i < $length; $i++) {
1099                $bytes[$i] = ~$bytes[$i];
1100            }
1101        }
1102        $int = hexdec(bin2hex($bytes));
1103
1104        $isBigInt = is_float($int);
1105        $this->throwIfBigIntGmpNeeded($isBigInt);
1106        if ($isBigInt) {
1107            $int = gmp_import($bytes);
1108        }
1109        $bytes = null;
1110
1111        # Complete Two's Complement by adding 1 and turning it negative...
1112        if ($isNegative) {
1113            $int = $isBigInt ? gmp_neg(gmp_add($int, '1')) : ($int + 1) * -1;
1114        }
1115
1116        return $isBigInt ? gmp_strval($int) : $int;
1117    }
1118
1119    /**
1120     * @param bool $isBigInt
1121     * @throws EncoderException
1122     */
1123    protected function throwIfBigIntGmpNeeded(bool $isBigInt): void
1124    {
1125        if ($isBigInt && !$this->isGmpAvailable) {
1126            throw new EncoderException(sprintf(
1127                'An integer higher than PHP_INT_MAX int (%s) was encountered and the GMP extension is not loaded.',
1128                PHP_INT_MAX
1129            ));
1130        }
1131    }
1132
1133    /**
1134     * @param int $length
1135     * @return float
1136     * @throws EncoderException
1137     */
1138    protected function decodeReal($length): float
1139    {
1140        if ($length === 0) {
1141            return 0;
1142        }
1143
1144        $ident = ord($this->binary[$this->pos++]);
1145        if ($ident === 0x40) {
1146            return INF;
1147        }
1148        if ($ident === 0x41) {
1149            return -INF;
1150        }
1151
1152        // @todo Real type encoding/decoding is rather complex. Need to implement this yet.
1153        throw new EncoderException('The real type encoding encountered is not yet implemented.');
1154    }
1155
1156    /**
1157     * Encoding subsets may require specific ordering on set types. Allow this to be overridden.
1158     *
1159     * @param SetType $set
1160     * @return string
1161     * @throws EncoderException
1162     */
1163    protected function encodeSet(SetType $set)
1164    {
1165        return $this->encodeConstructedType(...$set->getChildren());
1166    }
1167
1168    /**
1169     * Encoding subsets may require specific ordering on set of types. Allow this to be overridden.
1170     *
1171     * @param SetOfType $set
1172     * @return string
1173     * @throws EncoderException
1174     */
1175    protected function encodeSetOf(SetOfType $set)
1176    {
1177        return $this->encodeConstructedType(...$set->getChildren());
1178    }
1179
1180    /**
1181     * @param AbstractType ...$types
1182     * @return string
1183     * @throws EncoderException
1184     */
1185    protected function encodeConstructedType(AbstractType ...$types)
1186    {
1187        $bytes = '';
1188
1189        foreach ($types as $type) {
1190            $bytes .= $this->encode($type);
1191        }
1192
1193        return $bytes;
1194    }
1195
1196    /**
1197     * @param int $length
1198     * @return array
1199     * @throws EncoderException
1200     * @throws PartialPduException
1201     */
1202    protected function decodeConstructedType($length)
1203    {
1204        $children = [];
1205        $endAt = $this->pos + $length;
1206
1207        while ($this->pos < $endAt) {
1208            $children[] = $this->decodeBytes();
1209        }
1210
1211        return $children;
1212    }
1213}
1214