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