Skip to content

Commit

Permalink
Support arbitrary-precision for Integer/Enumerated type values.
Browse files Browse the repository at this point in the history
  • Loading branch information
ChadSikorra committed Sep 16, 2018
1 parent 29bf10d commit eb1d0c3
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 21 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"require-dev": {
"phpspec/phpspec": "^4.0"
},
"suggest": {
"ext-gmp": "For big integer support in Integer/Enumerated types."
},
"config": {
"bin-dir": "bin"
},
Expand Down
62 changes: 49 additions & 13 deletions src/FreeDSx/Asn1/Encoder/BerEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,17 @@ class BerEncoder implements EncoderInterface
],
];

/**
* @var bool
*/
protected $isGmpAvailable;

/**
* @param array $options
*/
public function __construct(array $options = [])
{
$this->isGmpAvailable = extension_loaded('gmp');
$this->setOptions($options);
}

Expand Down Expand Up @@ -485,7 +491,7 @@ protected function getDecodedTag($bytes, bool $isRoot) : array
# A high tag number is determined using VLQ (like the OID identifier encoding) of the subsequent bytes.
try {
$tagNumBytes = $this->getVlqBytes(substr($bytes, 1));
# It's possible we only got part of the VLQ for the high tag, as there is no way to know it's ending length.
# It's possible we only got part of the VLQ for the high tag, as there is no way to know it's ending length.
} catch (EncoderException $e) {
if ($isRoot) {
throw new PartialPduException(
Expand Down Expand Up @@ -576,7 +582,7 @@ protected function getEncodedTag(AbstractType $type)
# the VLV encoding of the tag number.
if ($type->getTagNumber() >= 31) {
$bytes = chr($tag | 0x1f).$this->intToVlqBytes($type->getTagNumber());
# For a tag less than 31, everything fits comfortably into a single byte.
# For a tag less than 31, everything fits comfortably into a single byte.
} else {
$bytes = chr($tag | $type->getTagNumber());
}
Expand Down Expand Up @@ -772,21 +778,30 @@ protected function formatDateTime(\DateTime $dateTime, string $dateTimeFormat, s
}

/**
* @param AbstractType $type
* @param AbstractType|IntegerType|EnumeratedType $type
* @return string
* @throws EncoderException
*/
protected function encodeInteger(AbstractType $type) : string
{
$int = abs($type->getValue());
$isNegative = ($type->getValue() < 0);
$isBigInt = $type->isBigInt();
$this->throwIfBigIntGmpNeeded($isBigInt);
$int = $isBigInt ? gmp_abs($type->getValue()) : abs((int) $type->getValue());
# Seems like a hack to check the big int this way...but probably the quickest
$isNegative = $isBigInt ? $type->getValue()[0] === '-' : ($type->getValue() < 0);

# Subtract one for Two's Complement...
if ($isNegative) {
$int = $int - 1;
$int = $isBigInt ? gmp_sub($int, '1') : $int - 1;
}

if ($isBigInt) {
$bytes = gmp_export($int);
} else {
# dechex can produce uneven hex while hex2bin requires it to be even
$hex = dechex($int);
$bytes = hex2bin((strlen($hex) % 2) === 0 ? $hex : '0' . $hex);
}
# dechex can produce uneven hex while hex2bin requires it to be even
$hex = dechex($int);
$bytes = hex2bin((strlen($hex) % 2) === 0 ? $hex : '0'.$hex);

# Two's Complement, invert the bits...
if ($isNegative) {
Expand Down Expand Up @@ -1013,9 +1028,10 @@ protected function binaryToBitString($bytes, int $length, int $unused) : string

/**
* @param string $bytes
* @return int number
* @return string|int number
* @throws EncoderException
*/
protected function decodeInteger($bytes) : int
protected function decodeInteger($bytes)
{
$isNegative = (ord($bytes[0]) & 0x80);
$len = strlen($bytes);
Expand All @@ -1028,12 +1044,32 @@ protected function decodeInteger($bytes) : int
}
$int = hexdec(bin2hex($bytes));

$isBigInt = is_float($int);
$this->throwIfBigIntGmpNeeded($isBigInt);
if ($isBigInt) {
$int = gmp_import($bytes);
}

# Complete Two's Complement by adding 1 and turning it negative...
if ($isNegative) {
$int = ($int + 1) * -1;
$int = $isBigInt ? gmp_neg(gmp_add($int, "1")) : ($int + 1) * -1;
}

return $int;
return $isBigInt ? gmp_strval($int) : $int;
}

/**
* @param bool $isBigInt
* @throws EncoderException
*/
protected function throwIfBigIntGmpNeeded(bool $isBigInt) : void
{
if ($isBigInt && !$this->isGmpAvailable) {
throw new EncoderException(sprintf(
'An integer higher than PHP_INT_MAX int (%s) was encountered and the GMP extension is not loaded.',
PHP_INT_MAX
));
}
}

/**
Expand Down
53 changes: 53 additions & 0 deletions src/FreeDSx/Asn1/Type/BigIntTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* This file is part of the FreeDSx ASN1 package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Asn1\Type;

use FreeDSx\Asn1\Exception\InvalidArgumentException;

/**
* Functionality needed between integer / enums for big int validation / checking.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
trait BigIntTrait
{
/**
* Whether or not the contained value is larger than the PHP_INT_MAX value (represented as a string value).
*
* @return bool
*/
public function isBigInt() : bool
{
if (is_int($this->value)) {
return false;
}

return is_float($this->value + 0);
}

/**
* @param $integer
*/
protected function validate($integer) : void
{
if (is_int($integer)) {
return;
}
if (is_string($integer) && is_numeric($integer) && strpos($integer, '.') === false) {
return;
}

throw new InvalidArgumentException(sprintf(
'The value passed to the %s class must be numeric.',
get_called_class()
));
}
}
13 changes: 8 additions & 5 deletions src/FreeDSx/Asn1/Type/EnumeratedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@
*/
class EnumeratedType extends AbstractType
{
use BigIntTrait;

protected $tagNumber = self::TAG_TYPE_ENUMERATED;

/**
* EnumeratedType constructor.
* @param int $enumValue
* @param string|int $enumValue
*/
public function __construct(int $enumValue)
public function __construct($enumValue)
{
$this->validate($enumValue);
parent::__construct($enumValue);
}

/**
* @param int $value
* @param string|int $value
* @return $this
*/
public function setValue(int $value)
public function setValue($value)
{
$this->validate($value);
$this->value = $value;

return $this;
Expand Down
10 changes: 7 additions & 3 deletions src/FreeDSx/Asn1/Type/IntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@
*/
class IntegerType extends AbstractType
{
use BigIntTrait;

protected $tagNumber = self::TAG_TYPE_INTEGER;

public function __construct(int $integer)
public function __construct($integer)
{
$this->validate($integer);
parent::__construct($integer);
}

/**
* @param int $value
* @param int|string $value
* @return $this
*/
public function setValue(int $value)
public function setValue($value)
{
$this->validate($value);
$this->value = $value;

return $this;
Expand Down
37 changes: 37 additions & 0 deletions tests/spec/FreeDSx/Asn1/Encoder/BerEncoderSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use FreeDSx\Asn1\Type\Utf8StringType;
use FreeDSx\Asn1\Type\VideotexStringType;
use FreeDSx\Asn1\Type\VisibleStringType;
use PhpSpec\Exception\Example\SkippingException;
use PhpSpec\ObjectBehavior;

class BerEncoderSpec extends ObjectBehavior
Expand Down Expand Up @@ -151,6 +152,11 @@ function it_should_encode_a_zero_integer_type()
$this->encode(new IntegerType(0))->shouldBeEqualTo(hex2bin('020100'));
}

function it_should_decode_a_big_int_positive_integer_type()
{
$this->decode(hex2bin('020900ffffffffffffffff'))->shouldBeLike(new IntegerType('18446744073709551615'));
}

function it_should_decode_a_positive_integer_type()
{
$this->decode(hex2bin('02087FFFFFFFFFFFFFFF'))->shouldBeLike(new IntegerType(9223372036854775807));
Expand All @@ -165,6 +171,11 @@ function it_should_decode_a_positive_integer_type()
$this->decode(hex2bin('02020080'))->shouldBeLike(new IntegerType(128));
}

function it_should_encode_a_big_int_positive_integer_type()
{
$this->encode(new IntegerType('18446744073709551615'))->shouldBeEqualTo(hex2bin('020900ffffffffffffffff'));
}

function it_should_encode_a_positive_integer_type()
{
$this->encode(new IntegerType(9223372036854775807))->shouldBeEqualTo(hex2bin('02087FFFFFFFFFFFFFFF'));
Expand All @@ -179,6 +190,11 @@ function it_should_encode_a_positive_integer_type()
$this->encode(new IntegerType(128))->shouldBeEqualTo(hex2bin('02020080'));
}

function it_should_decode_a_big_int_negative_integer_type()
{
$this->decode(hex2bin('0209ff0000000000000001'))->shouldBeLike(new IntegerType('-18446744073709551615'));
}

function it_should_decode_a_negative_integer_type()
{
$this->decode(hex2bin('02088000000000000001'))->shouldBeLike(new IntegerType(-9223372036854775807));
Expand All @@ -194,6 +210,11 @@ function it_should_decode_a_negative_integer_type()
$this->decode(hex2bin('0201FF'))->shouldBeLike(new IntegerType(-1));
}

function it_should_encode_a_big_int_negative_integer_type()
{
$this->encode(new IntegerType('-18446744073709551615'))->shouldBeEqualTo(hex2bin('0209ff0000000000000001'));
}

function it_should_encode_a_negative_integer_type()
{
$this->encode(new IntegerType(-9223372036854775807))->shouldBeEqualTo(hex2bin('02088000000000000001'));
Expand Down Expand Up @@ -807,4 +828,20 @@ function it_should_throw_an_exception_on_a_primitive_set()
{
$this->shouldThrow(EncoderException::class)->during('decode', [hex2bin('11030101ff')]);
}

function it_should_throw_an_exception_if_the_integer_to_encode_is_a_big_int_and_gmp_is_not_available()
{
if (extension_loaded('gmp')) {
throw new SkippingException('Only valid when GMP is not loaded.');
}
$this->shouldThrow(EncoderException::class)->during('encode', [new IntegerType('18446744073709551615')]);
}

function it_should_throw_an_exception_if_the_integer_to_decode_is_a_big_int_and_gmp_is_not_available()
{
if (extension_loaded('gmp')) {
throw new SkippingException('Only valid when GMP is not loaded.');
}
$this->shouldThrow(EncoderException::class)->during('decode', [hex2bin('020900ffffffffffffffff')]);
}
}
26 changes: 26 additions & 0 deletions tests/spec/FreeDSx/Asn1/Type/EnumeratedTypeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace spec\FreeDSx\Asn1\Type;

use FreeDSx\Asn1\Exception\InvalidArgumentException;
use FreeDSx\Asn1\Type\AbstractType;
use FreeDSx\Asn1\Type\EnumeratedType;
use PhpSpec\ObjectBehavior;
Expand Down Expand Up @@ -37,4 +38,29 @@ function it_should_have_a_default_tag_type()
{
$this->getTagNumber()->shouldBeEqualTo(AbstractType::TAG_TYPE_ENUMERATED);
}

function it_should_be_constructed_with_a_string_big_int()
{
$this->beConstructedWith("99999999999999999999999999999999999999");
$this->getValue()->shouldBeEqualTo("99999999999999999999999999999999999999");
}

function it_should_check_whether_the_value_is_a_bigint_or_not()
{
$this->isBigInt()->shouldBeEqualTo(false);
$this->setValue("99999999999999999999999999999999999999");
$this->isBigInt()->shouldBeEqualTo(true);
}

function it_should_throw_an_error_on_construction_if_the_value_is_not_numeric()
{
$this->shouldThrow(InvalidArgumentException::class)->during('__construct', ['foo']);
$this->shouldThrow(InvalidArgumentException::class)->during('__construct', ['1.5']);
}

function it_should_throw_an_error_on_set_if_the_value_is_not_numeric()
{
$this->shouldThrow(InvalidArgumentException::class)->during('setValue', ['foo']);
$this->shouldThrow(InvalidArgumentException::class)->during('setValue', ['1.5']);
}
}
26 changes: 26 additions & 0 deletions tests/spec/FreeDSx/Asn1/Type/IntegerTypeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace spec\FreeDSx\Asn1\Type;

use FreeDSx\Asn1\Exception\InvalidArgumentException;
use FreeDSx\Asn1\Type\AbstractType;
use FreeDSx\Asn1\Type\IntegerType;
use PhpSpec\ObjectBehavior;
Expand Down Expand Up @@ -37,4 +38,29 @@ function it_should_have_a_default_tag_type()
{
$this->getTagNumber()->shouldBeEqualTo(AbstractType::TAG_TYPE_INTEGER);
}

function it_should_be_constructed_with_a_string_big_int()
{
$this->beConstructedWith("99999999999999999999999999999999999999");
$this->getValue()->shouldBeEqualTo("99999999999999999999999999999999999999");
}

function it_should_check_whether_the_value_is_a_bigint_or_not()
{
$this->isBigInt()->shouldBeEqualTo(false);
$this->setValue("99999999999999999999999999999999999999");
$this->isBigInt()->shouldBeEqualTo(true);
}

function it_should_throw_an_error_on_construction_if_the_value_is_not_numeric()
{
$this->shouldThrow(InvalidArgumentException::class)->during('__construct', ['foo']);
$this->shouldThrow(InvalidArgumentException::class)->during('__construct', ['1.5']);
}

function it_should_throw_an_error_on_set_if_the_value_is_not_numeric()
{
$this->shouldThrow(InvalidArgumentException::class)->during('setValue', ['foo']);
$this->shouldThrow(InvalidArgumentException::class)->during('setValue', ['1.5']);
}
}

0 comments on commit eb1d0c3

Please sign in to comment.