GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Real::_encodeDecimal()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Sop\ASN1\Type\Primitive;
6
7
use Sop\ASN1\Component\Identifier;
8
use Sop\ASN1\Component\Length;
9
use Sop\ASN1\Element;
10
use Sop\ASN1\Exception\DecodeException;
11
use Sop\ASN1\Feature\ElementBase;
12
use Sop\ASN1\Type\PrimitiveType;
13
use Sop\ASN1\Type\UniversalClass;
14
use Sop\ASN1\Util\BigInt;
15
16
/**
17
 * Implements *REAL* type.
18
 */
19
class Real extends Element
20
{
21
    use UniversalClass;
22
    use PrimitiveType;
23
24
    /**
25
     * Regex pattern to parse NR1 form number.
26
     *
27
     * @var string
28
     */
29
    const NR1_REGEX = '/^\s*' .
30
        '(?<s>[+\-])?' .    // sign
31
        '(?<i>\d+)' .       // integer
32
    '$/';
33
34
    /**
35
     * Regex pattern to parse NR2 form number.
36
     *
37
     * @var string
38
     */
39
    const NR2_REGEX = '/^\s*' .
40
        '(?<s>[+\-])?' .                            // sign
41
        '(?<d>(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' .   // decimal number
42
    '$/';
43
44
    /**
45
     * Regex pattern to parse NR3 form number.
46
     *
47
     * @var string
48
     */
49
    const NR3_REGEX = '/^\s*' .
50
        '(?<ms>[+\-])?' .                           // mantissa sign
51
        '(?<m>(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' .   // mantissa
52
        '[Ee](?<es>[+\-])?' .                       // exponent sign
53
        '(?<e>\d+)' .                               // exponent
54
    '$/';
55
56
    /**
57
     * Regex pattern to parse PHP exponent number format.
58
     *
59
     * @see http://php.net/manual/en/language.types.float.php
60
     *
61
     * @var string
62
     */
63
    const PHP_EXPONENT_DNUM = '/^' .
64
        '(?<ms>[+\-])?' .               // sign
65
        '(?<m>' .
66
            '\d+' .                     // LNUM
67
            '|' .
68
            '(?:\d*\.\d+|\d+\.\d*)' .   // DNUM
69
        ')[eE]' .
70
        '(?<es>[+\-])?(?<e>\d+)' .      // exponent
71
    '$/';
72
73
    /**
74
     * Exponent when value is positive or negative infinite.
75
     *
76
     * @var int
77
     */
78
    const INF_EXPONENT = 2047;
79
80
    /**
81
     * Exponent bias for IEEE 754 double precision float.
82
     *
83
     * @var int
84
     */
85
    const EXP_BIAS = -1023;
86
87
    /**
88
     * Signed integer mantissa.
89
     *
90
     * @var BigInt
91
     */
92
    private $_mantissa;
93
94
    /**
95
     * Signed integer exponent.
96
     *
97
     * @var BigInt
98
     */
99
    private $_exponent;
100
101
    /**
102
     * Abstract value base.
103
     *
104
     * Must be 2 or 10.
105
     *
106
     * @var int
107
     */
108
    private $_base;
109
110
    /**
111
     * Whether to encode strictly in DER.
112
     *
113
     * @var bool
114
     */
115
    private $_strictDer;
116
117
    /**
118
     * Number as a native float.
119
     *
120
     * @internal Lazily initialized
121
     *
122
     * @var null|float
123
     */
124
    private $_float;
125
126
    /**
127
     * Constructor.
128
     *
129
     * @param \GMP|int|string $mantissa Integer mantissa
130
     * @param \GMP|int|string $exponent Integer exponent
131
     * @param int             $base     Base, 2 or 10
132
     */
133 56
    public function __construct($mantissa, $exponent, int $base = 10)
134
    {
135 56
        if (10 !== $base && 2 !== $base) {
136 1
            throw new \UnexpectedValueException('Base must be 2 or 10.');
137
        }
138 55
        $this->_typeTag = self::TYPE_REAL;
139 55
        $this->_strictDer = true;
140 55
        $this->_mantissa = new BigInt($mantissa);
141 55
        $this->_exponent = new BigInt($exponent);
142 55
        $this->_base = $base;
143 55
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 1
    public function __toString(): string
149
    {
150 1
        return sprintf('%g', $this->floatVal());
151
    }
152
153
    /**
154
     * Create base 2 real number from float.
155
     */
156 36
    public static function fromFloat(float $number): self
157
    {
158 36
        if (is_infinite($number)) {
159 4
            return self::_fromInfinite($number);
160
        }
161 32
        if (is_nan($number)) {
162 1
            throw new \UnexpectedValueException('NaN values not supported.');
163
        }
164 31
        [$m, $e] = self::_parse754Double(pack('E', $number));
165 31
        return new self($m, $e, 2);
166
    }
167
168
    /**
169
     * Create base 10 real number from string.
170
     *
171
     * @param string $number Real number in base-10 textual form
172
     */
173 10
    public static function fromString(string $number): self
174
    {
175 10
        [$m, $e] = self::_parseString($number);
176 9
        return new self($m, $e, 10);
177
    }
178
179
    /**
180
     * Get self with strict DER flag set or unset.
181
     *
182
     * @param bool $strict whether to encode strictly in DER
183
     */
184 20
    public function withStrictDER(bool $strict): self
185
    {
186 20
        $obj = clone $this;
187 20
        $obj->_strictDer = $strict;
188 20
        return $obj;
189
    }
190
191
    /**
192
     * Get the mantissa.
193
     */
194 1
    public function mantissa(): BigInt
195
    {
196 1
        return $this->_mantissa;
197
    }
198
199
    /**
200
     * Get the exponent.
201
     */
202 3
    public function exponent(): BigInt
203
    {
204 3
        return $this->_exponent;
205
    }
206
207
    /**
208
     * Get the base.
209
     */
210 1
    public function base(): int
211
    {
212 1
        return $this->_base;
213
    }
214
215
    /**
216
     * Get number as a float.
217
     */
218 46
    public function floatVal(): float
219
    {
220 46
        if (!isset($this->_float)) {
221 46
            $m = $this->_mantissa->intVal();
222 46
            $e = $this->_exponent->intVal();
223 46
            $this->_float = (float) ($m * pow($this->_base, $e));
224
        }
225 46
        return $this->_float;
226
    }
227
228
    /**
229
     * Get number as a NR3 form string conforming to DER rules.
230
     */
231 5
    public function nr3Val(): string
232
    {
233
        // convert to base 10
234 5
        if (2 === $this->_base) {
235 1
            [$m, $e] = self::_parseString(sprintf('%15E', $this->floatVal()));
236
        } else {
237 4
            $m = $this->_mantissa->gmpObj();
238 4
            $e = $this->_exponent->gmpObj();
239
        }
240
        // shift trailing zeroes from the mantissa to the exponent
241
        // (X.690 07-2002, section 11.3.2.4)
242 5
        while (0 != $m && 0 == $m % 10) {
243 1
            $m /= 10;
244 1
            ++$e;
245
        }
246
        // if exponent is zero, it must be prefixed with a "+" sign
247
        // (X.690 07-2002, section 11.3.2.6)
248 5
        if (0 == $e) {
249 1
            $es = '+';
250
        } else {
251 4
            $es = $e < 0 ? '-' : '';
252
        }
253 5
        return sprintf('%s.E%s%s', gmp_strval($m), $es, gmp_strval(gmp_abs($e)));
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 43
    protected function _encodedContentDER(): string
260
    {
261 43
        if (self::INF_EXPONENT == $this->_exponent->gmpObj()) {
262 5
            return $this->_encodeSpecial();
263
        }
264
        // if the real value is the value zero, there shall be no contents
265
        // octets in the encoding. (X.690 07-2002, section 8.5.2)
266 38
        if (0 == $this->_mantissa->gmpObj()) {
267 2
            return '';
268
        }
269 36
        if (10 === $this->_base) {
270 1
            return $this->_encodeDecimal();
271
        }
272 35
        return $this->_encodeBinary();
273
    }
274
275
    /**
276
     * Encode in binary format.
277
     */
278 35
    protected function _encodeBinary(): string
279
    {
280 35
        [$base, $sign, $m, $e] = $this->_prepareBinaryEncoding();
281 35
        $zero = gmp_init(0, 10);
282 35
        $byte = 0x80;
283 35
        if ($sign < 0) {
284 14
            $byte |= 0x40;
285
        }
286
        // normalization: mantissa must be 0 or odd
287 35
        if (2 === $base) {
288
            // while last bit is zero
289 22
            while ($m > 0 && 0 === gmp_cmp($m & 0x01, $zero)) {
290 1
                $m >>= 1;
291 1
                ++$e;
292
            }
293 13
        } elseif (8 === $base) {
294 5
            $byte |= 0x10;
295
            // while last 3 bits are zero
296 5
            while ($m > 0 && 0 === gmp_cmp($m & 0x07, $zero)) {
297 1
                $m >>= 3;
298 1
                ++$e;
299
            }
300
        } else { // base === 16
301 8
            $byte |= 0x20;
302
            // while last 4 bits are zero
303 8
            while ($m > 0 && 0 === gmp_cmp($m & 0x0f, $zero)) {
304 2
                $m >>= 4;
305 2
                ++$e;
306
            }
307
        }
308
        // scale factor
309 35
        $scale = 0;
310 35
        while ($m > 0 && 0 === gmp_cmp($m & 0x01, $zero)) {
311 1
            $m >>= 1;
312 1
            ++$scale;
313
        }
314 35
        $byte |= ($scale & 0x03) << 2;
315
        // encode exponent
316 35
        $exp_bytes = (new BigInt($e))->signedOctets();
317 35
        $exp_len = strlen($exp_bytes);
318 35
        if ($exp_len > 0xff) {
319 1
            throw new \RangeException('Exponent encoding is too long.');
320
        }
321 34
        if ($exp_len <= 3) {
322 32
            $byte |= ($exp_len - 1) & 0x03;
323 32
            $bytes = chr($byte);
324
        } else {
325 2
            $byte |= 0x03;
326 2
            $bytes = chr($byte) . chr($exp_len);
327
        }
328 34
        $bytes .= $exp_bytes;
329
        // encode mantissa
330 34
        $bytes .= (new BigInt($m))->unsignedOctets();
331 34
        return $bytes;
332
    }
333
334
    /**
335
     * Encode in decimal format.
336
     */
337 1
    protected function _encodeDecimal(): string
338
    {
339
        // encode in NR3 decimal encoding
340 1
        return chr(0x03) . $this->nr3Val();
341
    }
342
343
    /**
344
     * Encode special value.
345
     */
346 5
    protected function _encodeSpecial(): string
347
    {
348 5
        switch ($this->_mantissa->intVal()) {
349
            // positive infitinity
350 5
            case 1:
351 2
                return chr(0x40);
352
            // negative infinity
353
            case -1:
354 2
                return chr(0x41);
355
        }
356 1
        throw new \LogicException('Invalid special value.');
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362 47
    protected static function _decodeFromDER(Identifier $identifier,
363
        string $data, int &$offset): ElementBase
364
    {
365 47
        $idx = $offset;
366 47
        $length = Length::expectFromDER($data, $idx)->intLength();
367
        // if length is zero, value is zero (spec 8.5.2)
368 47
        if (!$length) {
369 2
            $obj = new self(0, 0, 10);
370
        } else {
371 45
            $bytes = substr($data, $idx, $length);
372 45
            $byte = ord($bytes[0]);
373 45
            if (0x80 & $byte) { // bit 8 = 1
374 37
                $obj = self::_decodeBinaryEncoding($bytes);
375 8
            } elseif (0x00 === $byte >> 6) { // bit 8 = 0, bit 7 = 0
376 2
                $obj = self::_decodeDecimalEncoding($bytes);
377
            } else { // bit 8 = 0, bit 7 = 1
378 6
                $obj = self::_decodeSpecialRealValue($bytes);
379
            }
380
        }
381 40
        $offset = $idx + $length;
382 40
        return $obj;
383
    }
384
385
    /**
386
     * Decode binary encoding.
387
     */
388 37
    protected static function _decodeBinaryEncoding(string $data)
389
    {
390 37
        $byte = ord($data[0]);
391
        // bit 7 is set if mantissa is negative
392 37
        $neg = (bool) (0x40 & $byte);
393
        // encoding base in bits 6 and 5
394 37
        switch (($byte >> 4) & 0x03) {
395 37
            case 0b00:
396 23
                $base = 2;
397 23
                break;
398 14
            case 0b01:
399 5
                $base = 8;
400 5
                break;
401 9
            case 0b10:
402 8
                $base = 16;
403 8
                break;
404
            default:
405 1
                throw new DecodeException(
406 1
                    'Reserved REAL binary encoding base not supported.');
407
        }
408
        // scaling factor in bits 4 and 3
409 36
        $scale = ($byte >> 2) & 0x03;
410 36
        $idx = 1;
411
        // content length in bits 2 and 1
412 36
        $len = ($byte & 0x03) + 1;
413
        // if both bits are set, the next octet encodes the length
414 36
        if ($len > 3) {
415 2
            if (strlen($data) < 2) {
416 1
                throw new DecodeException(
417 1
                    'Unexpected end of data while decoding REAL exponent length.');
418
            }
419 1
            $len = ord($data[1]);
420 1
            $idx = 2;
421
        }
422 35
        if (strlen($data) < $idx + $len) {
423 1
            throw new DecodeException(
424 1
                'Unexpected end of data while decoding REAL exponent.');
425
        }
426
        // decode exponent
427 34
        $octets = substr($data, $idx, $len);
428 34
        $exp = BigInt::fromSignedOctets($octets)->gmpObj();
429 34
        if (8 === $base) {
430 5
            $exp *= 3;
431 29
        } elseif (16 === $base) {
432 8
            $exp *= 4;
433
        }
434 34
        if (strlen($data) <= $idx + $len) {
435 1
            throw new DecodeException(
436 1
                'Unexpected end of data while decoding REAL mantissa.');
437
        }
438
        // decode mantissa
439 33
        $octets = substr($data, $idx + $len);
440 33
        $n = BigInt::fromUnsignedOctets($octets)->gmpObj();
441 33
        $n *= 2 ** $scale;
442 33
        if ($neg) {
443 14
            $n = gmp_neg($n);
444
        }
445 33
        return new self($n, $exp, 2);
446
    }
447
448
    /**
449
     * Decode decimal encoding.
450
     *
451
     * @throws \RuntimeException
452
     */
453 2
    protected static function _decodeDecimalEncoding(string $data): self
454
    {
455 2
        $nr = ord($data[0]) & 0x3f;
456 2
        if (!in_array($nr, [1, 2, 3])) {
457 1
            throw new DecodeException('Unsupported decimal encoding form.');
458
        }
459 1
        $str = substr($data, 1);
460 1
        return self::fromString($str);
461
    }
462
463
    /**
464
     * Decode special encoding.
465
     */
466 6
    protected static function _decodeSpecialRealValue(string $data): self
467
    {
468 6
        if (1 !== strlen($data)) {
469 1
            throw new DecodeException(
470 1
                'SpecialRealValue must have one content octet.');
471
        }
472 5
        $byte = ord($data[0]);
473 5
        if (0x40 === $byte) {   // positive infinity
474 2
            return self::_fromInfinite(INF);
475
        }
476 3
        if (0x41 === $byte) {   // negative infinity
477 2
            return self::_fromInfinite(-INF);
478
        }
479 1
        throw new DecodeException('Invalid SpecialRealValue encoding.');
480
    }
481
482
    /**
483
     * Prepare value for binary encoding.
484
     *
485
     * @return array (int) base, (int) sign, (\GMP) mantissa and (\GMP) exponent
486
     */
487 35
    protected function _prepareBinaryEncoding(): array
488
    {
489 35
        $base = 2;
490 35
        $m = $this->_mantissa->gmpObj();
491 35
        $ms = gmp_sign($m);
492 35
        $m = gmp_abs($m);
493 35
        $e = $this->_exponent->gmpObj();
494 35
        $es = gmp_sign($e);
495 35
        $e = gmp_abs($e);
496
        // DER uses only base 2 binary encoding
497 35
        if (!$this->_strictDer) {
498 17
            if (0 == $e % 4) {
499 8
                $base = 16;
500 8
                $e = gmp_div_q($e, 4);
501 9
            } elseif (0 == $e % 3) {
502 5
                $base = 8;
503 5
                $e = gmp_div_q($e, 3);
504
            }
505
        }
506 35
        return [$base, $ms, $m, $e * $es];
507
    }
508
509
    /**
510
     * Initialize from INF or -INF.
511
     */
512 4
    private static function _fromInfinite(float $inf): self
513
    {
514 4
        return new self($inf === -INF ? -1 : 1, self::INF_EXPONENT, 2);
515
    }
516
517
    /**
518
     * Parse IEEE 754 big endian formatted double precision float to base 2
519
     * mantissa and exponent.
520
     *
521
     * @param string $octets 64 bits
522
     *
523
     * @return \GMP[] Tuple of mantissa and exponent
524
     */
525 31
    private static function _parse754Double(string $octets): array
526
    {
527 31
        $n = gmp_import($octets, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN);
528
        // sign bit
529 31
        $neg = gmp_testbit($n, 63);
530
        // 11 bits of biased exponent
531 31
        $exp = (gmp_and($n, '0x7ff0000000000000') >> 52) + self::EXP_BIAS;
532
        // 52 bits of mantissa
533 31
        $man = gmp_and($n, '0xfffffffffffff');
534
        // zero, ASN.1 doesn't differentiate -0 from +0
535 31
        if (self::EXP_BIAS == $exp && 0 == $man) {
536 2
            return [gmp_init(0, 10), gmp_init(0, 10)];
537
        }
538
        // denormalized value, shift binary point
539 29
        if (self::EXP_BIAS == $exp) {
540 4
            ++$exp;
541
        }
542
        // normalized value, insert implicit leading one before the binary point
543
        else {
544 25
            gmp_setbit($man, 52);
545
        }
546
        // find the last fraction bit that is set
547 29
        $last = gmp_scan1($man, 0);
548 29
        $bits_for_fraction = 52 - $last;
549
        // adjust mantissa and exponent so that we have integer values
550 29
        $man >>= $last;
551 29
        $exp -= $bits_for_fraction;
552
        // negate mantissa if number was negative
553 29
        if ($neg) {
554 15
            $man = gmp_neg($man);
555
        }
556 29
        return [$man, $exp];
557
    }
558
559
    /**
560
     * Parse textual REAL number to base 10 mantissa and exponent.
561
     *
562
     * @param string $str Number
563
     *
564
     * @return \GMP[] Tuple of mantissa and exponent
565
     */
566 11
    private static function _parseString(string $str): array
567
    {
568
        // PHP exponent format
569 11
        if (preg_match(self::PHP_EXPONENT_DNUM, $str, $match)) {
570 2
            [$m, $e] = self::_parsePHPExponentMatch($match);
571
        }
572
        // NR3 format
573 9
        elseif (preg_match(self::NR3_REGEX, $str, $match)) {
574 3
            [$m, $e] = self::_parseNR3Match($match);
575
        }
576
        // NR2 format
577 6
        elseif (preg_match(self::NR2_REGEX, $str, $match)) {
578 2
            [$m, $e] = self::_parseNR2Match($match);
579
        }
580
        // NR1 format
581 4
        elseif (preg_match(self::NR1_REGEX, $str, $match)) {
582 3
            [$m, $e] = self::_parseNR1Match($match);
583
        }
584
        // invalid number
585
        else {
586 1
            throw new \UnexpectedValueException(
587 1
                "{$str} could not be parsed to REAL.");
588
        }
589
        // normalize so that mantsissa has no trailing zeroes
590 10
        while (0 != $m && 0 == $m % 10) {
591 1
            $m /= 10;
592 1
            ++$e;
593
        }
594 10
        return [$m, $e];
595
    }
596
597
    /**
598
     * Parse PHP form float to base 10 mantissa and exponent.
599
     *
600
     * @param array $match Regexp match
601
     *
602
     * @return \GMP[] Tuple of mantissa and exponent
603
     */
604 2
    private static function _parsePHPExponentMatch(array $match): array
605
    {
606
        // mantissa sign
607 2
        $ms = '-' === $match['ms'] ? -1 : 1;
608 2
        $m_parts = explode('.', $match['m']);
609
        // integer part of the mantissa
610 2
        $int = ltrim($m_parts[0], '0');
611
        // exponent sign
612 2
        $es = '-' === $match['es'] ? -1 : 1;
613
        // signed exponent
614 2
        $e = gmp_init($match['e'], 10) * $es;
615
        // if mantissa had fractional part
616 2
        if (2 === count($m_parts)) {
617 2
            $frac = rtrim($m_parts[1], '0');
618 2
            $e -= strlen($frac);
619 2
            $int .= $frac;
620
        }
621 2
        $m = gmp_init($int, 10) * $ms;
622 2
        return [$m, $e];
623
    }
624
625
    /**
626
     * Parse NR3 form number to base 10 mantissa and exponent.
627
     *
628
     * @param array $match Regexp match
629
     *
630
     * @return \GMP[] Tuple of mantissa and exponent
631
     */
632 3
    private static function _parseNR3Match(array $match): array
633
    {
634
        // mantissa sign
635 3
        $ms = '-' === $match['ms'] ? -1 : 1;
636
        // explode mantissa to integer and fraction parts
637 3
        [$int, $frac] = explode('.', str_replace(',', '.', $match['m']));
638 3
        $int = ltrim($int, '0');
639 3
        $frac = rtrim($frac, '0');
640
        // exponent sign
641 3
        $es = '-' === $match['es'] ? -1 : 1;
642
        // signed exponent
643 3
        $e = gmp_init($match['e'], 10) * $es;
644
        // shift exponent by the number of base 10 fractions
645 3
        $e -= strlen($frac);
646
        // insert fractions to integer part and produce signed mantissa
647 3
        $int .= $frac;
648 3
        if ('' === $int) {
649 1
            $int = '0';
650
        }
651 3
        $m = gmp_init($int, 10) * $ms;
652 3
        return [$m, $e];
653
    }
654
655
    /**
656
     * Parse NR2 form number to base 10 mantissa and exponent.
657
     *
658
     * @param array $match Regexp match
659
     *
660
     * @return \GMP[] Tuple of mantissa and exponent
661
     */
662 2
    private static function _parseNR2Match(array $match): array
663
    {
664 2
        $sign = '-' === $match['s'] ? -1 : 1;
665
        // explode decimal number to integer and fraction parts
666 2
        [$int, $frac] = explode('.', str_replace(',', '.', $match['d']));
667 2
        $int = ltrim($int, '0');
668 2
        $frac = rtrim($frac, '0');
669
        // shift exponent by the number of base 10 fractions
670 2
        $e = gmp_init(0, 10);
671 2
        $e -= strlen($frac);
672
        // insert fractions to integer part and produce signed mantissa
673 2
        $int .= $frac;
674 2
        if ('' === $int) {
675 1
            $int = '0';
676
        }
677 2
        $m = gmp_init($int, 10) * $sign;
678 2
        return [$m, $e];
679
    }
680
681
    /**
682
     * Parse NR1 form number to base 10 mantissa and exponent.
683
     *
684
     * @param array $match Regexp match
685
     *
686
     * @return \GMP[] Tuple of mantissa and exponent
687
     */
688 3
    private static function _parseNR1Match(array $match): array
689
    {
690 3
        $sign = '-' === $match['s'] ? -1 : 1;
691 3
        $int = ltrim($match['i'], '0');
692 3
        if ('' === $int) {
693 1
            $int = '0';
694
        }
695 3
        $m = gmp_init($int, 10) * $sign;
696 3
        return [$m, gmp_init(0, 10)];
697
    }
698
}
699