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