1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types = 1); |
4
|
|
|
|
5
|
|
|
namespace Sop\ASN1\Util; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Class to wrap an integer of arbirtary length. |
9
|
|
|
*/ |
10
|
|
|
class BigInt |
11
|
|
|
{ |
12
|
|
|
/** |
13
|
|
|
* Number as a GMP object. |
14
|
|
|
* |
15
|
|
|
* @var \GMP |
16
|
|
|
*/ |
17
|
|
|
private $_gmp; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Number as a base 10 integer string. |
21
|
|
|
* |
22
|
|
|
* @internal Lazily initialized |
23
|
|
|
* |
24
|
|
|
* @var null|string |
25
|
|
|
*/ |
26
|
|
|
private $_num; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Number as an integer type. |
30
|
|
|
* |
31
|
|
|
* @internal Lazily initialized |
32
|
|
|
* |
33
|
|
|
* @var null|int |
34
|
|
|
*/ |
35
|
|
|
private $_intNum; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Constructor. |
39
|
|
|
* |
40
|
|
|
* @param \GMP|int|string $num Integer number in base 10 |
41
|
|
|
*/ |
42
|
469 |
|
public function __construct($num) |
43
|
|
|
{ |
44
|
|
|
// convert to GMP object |
45
|
469 |
|
if (!($num instanceof \GMP)) { |
46
|
449 |
|
$gmp = @gmp_init($num, 10); |
47
|
449 |
|
if (false === $gmp) { |
48
|
1 |
|
throw new \InvalidArgumentException( |
49
|
1 |
|
"Unable to convert '{$num}' to integer."); |
50
|
|
|
} |
51
|
448 |
|
$num = $gmp; |
52
|
|
|
} |
53
|
468 |
|
$this->_gmp = $num; |
|
|
|
|
54
|
468 |
|
} |
55
|
|
|
|
56
|
1 |
|
public function __toString(): string |
57
|
|
|
{ |
58
|
1 |
|
return $this->base10(); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Initialize from an arbitrary length of octets as an unsigned integer. |
63
|
|
|
*/ |
64
|
35 |
|
public static function fromUnsignedOctets(string $octets): self |
65
|
|
|
{ |
66
|
35 |
|
if (!strlen($octets)) { |
67
|
1 |
|
throw new \InvalidArgumentException('Empty octets.'); |
68
|
|
|
} |
69
|
34 |
|
return new self(gmp_import($octets, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN)); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Initialize from an arbitrary length of octets as an signed integer |
74
|
|
|
* having two's complement encoding. |
75
|
|
|
*/ |
76
|
71 |
|
public static function fromSignedOctets(string $octets): self |
77
|
|
|
{ |
78
|
71 |
|
if (!strlen($octets)) { |
79
|
1 |
|
throw new \InvalidArgumentException('Empty octets.'); |
80
|
|
|
} |
81
|
70 |
|
$neg = ord($octets[0]) & 0x80; |
82
|
|
|
// negative, apply inversion of two's complement |
83
|
70 |
|
if ($neg) { |
84
|
32 |
|
$octets = ~$octets; |
85
|
|
|
} |
86
|
70 |
|
$num = gmp_import($octets, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
87
|
|
|
// negative, apply addition of two's complement and produce negative result |
88
|
70 |
|
if ($neg) { |
89
|
32 |
|
$num = gmp_neg($num + 1); |
90
|
|
|
} |
91
|
70 |
|
return new self($num); |
|
|
|
|
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Get the number as a base 10 integer string. |
96
|
|
|
*/ |
97
|
35 |
|
public function base10(): string |
98
|
|
|
{ |
99
|
35 |
|
if (!isset($this->_num)) { |
100
|
35 |
|
$this->_num = gmp_strval($this->_gmp, 10); |
101
|
|
|
} |
102
|
35 |
|
return $this->_num; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Get the number as an integer. |
107
|
|
|
* |
108
|
|
|
* @throws \RuntimeException If number overflows integer size |
109
|
|
|
*/ |
110
|
313 |
|
public function intVal(): int |
111
|
|
|
{ |
112
|
313 |
|
if (!isset($this->_intNum)) { |
113
|
311 |
|
if (gmp_cmp($this->_gmp, $this->_intMaxGmp()) > 0) { |
114
|
4 |
|
throw new \RuntimeException('Integer overflow.'); |
115
|
|
|
} |
116
|
307 |
|
if (gmp_cmp($this->_gmp, $this->_intMinGmp()) < 0) { |
117
|
1 |
|
throw new \RuntimeException('Integer underflow.'); |
118
|
|
|
} |
119
|
306 |
|
$this->_intNum = gmp_intval($this->_gmp); |
120
|
|
|
} |
121
|
308 |
|
return $this->_intNum; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Get the number as a `GMP` object. |
126
|
|
|
* |
127
|
|
|
* @throws \RuntimeException if number is not a valid integer |
128
|
|
|
*/ |
129
|
238 |
|
public function gmpObj(): \GMP |
130
|
|
|
{ |
131
|
238 |
|
return clone $this->_gmp; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Get the number as an unsigned integer encoded in binary. |
136
|
|
|
*/ |
137
|
35 |
|
public function unsignedOctets(): string |
138
|
|
|
{ |
139
|
35 |
|
return gmp_export($this->_gmp, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Get the number as a signed integer encoded in two's complement binary. |
144
|
|
|
*/ |
145
|
71 |
|
public function signedOctets(): string |
146
|
|
|
{ |
147
|
71 |
|
switch (gmp_sign($this->_gmp)) { |
148
|
71 |
|
case 1: |
149
|
31 |
|
return $this->_signedPositiveOctets(); |
150
|
|
|
case -1: |
151
|
33 |
|
return $this->_signedNegativeOctets(); |
152
|
|
|
} |
153
|
|
|
// zero |
154
|
7 |
|
return chr(0); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Encode positive integer in two's complement binary. |
159
|
|
|
*/ |
160
|
31 |
|
private function _signedPositiveOctets(): string |
161
|
|
|
{ |
162
|
31 |
|
$bin = gmp_export($this->_gmp, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
163
|
|
|
// if first bit is 1, prepend full zero byte to represent positive two's complement |
164
|
31 |
|
if (ord($bin[0]) & 0x80) { |
165
|
6 |
|
$bin = chr(0x00) . $bin; |
166
|
|
|
} |
167
|
31 |
|
return $bin; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Encode negative integer in two's complement binary. |
172
|
|
|
*/ |
173
|
33 |
|
private function _signedNegativeOctets(): string |
174
|
|
|
{ |
175
|
33 |
|
$num = gmp_abs($this->_gmp); |
176
|
|
|
// compute number of bytes required |
177
|
33 |
|
$width = 1; |
178
|
33 |
|
if ($num > 128) { |
179
|
24 |
|
$tmp = $num; |
180
|
|
|
do { |
181
|
24 |
|
++$width; |
182
|
24 |
|
$tmp >>= 8; |
183
|
24 |
|
} while ($tmp > 128); |
184
|
|
|
} |
185
|
|
|
// compute two's complement 2^n - x |
186
|
33 |
|
$num = gmp_pow('2', 8 * $width) - $num; |
187
|
33 |
|
$bin = gmp_export($num, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
188
|
|
|
// if first bit is 0, prepend full inverted byte to represent negative two's complement |
189
|
33 |
|
if (!(ord($bin[0]) & 0x80)) { |
190
|
2 |
|
$bin = chr(0xff) . $bin; |
191
|
|
|
} |
192
|
33 |
|
return $bin; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Get the maximum integer value. |
197
|
|
|
*/ |
198
|
311 |
|
private function _intMaxGmp(): \GMP |
199
|
|
|
{ |
200
|
311 |
|
static $gmp; |
201
|
311 |
|
if (!isset($gmp)) { |
202
|
1 |
|
$gmp = gmp_init(PHP_INT_MAX, 10); |
203
|
|
|
} |
204
|
311 |
|
return $gmp; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Get the minimum integer value. |
209
|
|
|
*/ |
210
|
307 |
|
private function _intMinGmp(): \GMP |
211
|
|
|
{ |
212
|
307 |
|
static $gmp; |
213
|
307 |
|
if (!isset($gmp)) { |
214
|
1 |
|
$gmp = gmp_init(PHP_INT_MIN, 10); |
215
|
|
|
} |
216
|
307 |
|
return $gmp; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.