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