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
|
|
|
/** |
57
|
|
|
* @return string |
58
|
|
|
*/ |
59
|
1 |
|
public function __toString(): string |
60
|
|
|
{ |
61
|
1 |
|
return $this->base10(); |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Initialize from an arbitrary length of octets as an unsigned integer. |
66
|
|
|
* |
67
|
|
|
* @param string $octets |
68
|
|
|
* |
69
|
|
|
* @return self |
70
|
|
|
*/ |
71
|
35 |
|
public static function fromUnsignedOctets(string $octets): self |
72
|
|
|
{ |
73
|
35 |
|
if (!strlen($octets)) { |
74
|
1 |
|
throw new \InvalidArgumentException('Empty octets.'); |
75
|
|
|
} |
76
|
34 |
|
return new self(gmp_import($octets, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN)); |
|
|
|
|
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Initialize from an arbitrary length of octets as an signed integer |
81
|
|
|
* having two's complement encoding. |
82
|
|
|
* |
83
|
|
|
* @param string $octets |
84
|
|
|
* |
85
|
|
|
* @return self |
86
|
|
|
*/ |
87
|
71 |
|
public static function fromSignedOctets(string $octets): self |
88
|
|
|
{ |
89
|
71 |
|
if (!strlen($octets)) { |
90
|
1 |
|
throw new \InvalidArgumentException('Empty octets.'); |
91
|
|
|
} |
92
|
70 |
|
$neg = ord($octets[0]) & 0x80; |
93
|
|
|
// negative, apply inversion of two's complement |
94
|
70 |
|
if ($neg) { |
95
|
32 |
|
$octets = ~$octets; |
96
|
|
|
} |
97
|
70 |
|
$num = gmp_import($octets, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
98
|
|
|
// negative, apply addition of two's complement and produce negative result |
99
|
70 |
|
if ($neg) { |
100
|
32 |
|
$num = gmp_neg($num + 1); |
101
|
|
|
} |
102
|
70 |
|
return new self($num); |
|
|
|
|
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Get the number as a base 10 integer string. |
107
|
|
|
* |
108
|
|
|
* @return string |
109
|
|
|
*/ |
110
|
35 |
|
public function base10(): string |
111
|
|
|
{ |
112
|
35 |
|
if (!isset($this->_num)) { |
113
|
35 |
|
$this->_num = gmp_strval($this->_gmp, 10); |
114
|
|
|
} |
115
|
35 |
|
return $this->_num; |
|
|
|
|
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Get the number as an integer. |
120
|
|
|
* |
121
|
|
|
* @throws \RuntimeException If number overflows integer size |
122
|
|
|
* |
123
|
|
|
* @return int |
124
|
|
|
*/ |
125
|
313 |
|
public function intVal(): int |
126
|
|
|
{ |
127
|
313 |
|
if (!isset($this->_intNum)) { |
128
|
311 |
|
if (gmp_cmp($this->_gmp, $this->_intMaxGmp()) > 0) { |
129
|
4 |
|
throw new \RuntimeException('Integer overflow.'); |
130
|
|
|
} |
131
|
307 |
|
if (gmp_cmp($this->_gmp, $this->_intMinGmp()) < 0) { |
132
|
1 |
|
throw new \RuntimeException('Integer underflow.'); |
133
|
|
|
} |
134
|
306 |
|
$this->_intNum = gmp_intval($this->_gmp); |
135
|
|
|
} |
136
|
308 |
|
return $this->_intNum; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Get the number as a `GMP` object. |
141
|
|
|
* |
142
|
|
|
* @throws \RuntimeException if number is not a valid integer |
143
|
|
|
* |
144
|
|
|
* @return \GMP |
145
|
|
|
*/ |
146
|
238 |
|
public function gmpObj(): \GMP |
147
|
|
|
{ |
148
|
238 |
|
return clone $this->_gmp; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Get the number as an unsigned integer encoded in binary. |
153
|
|
|
* |
154
|
|
|
* @return string |
155
|
|
|
*/ |
156
|
35 |
|
public function unsignedOctets(): string |
157
|
|
|
{ |
158
|
35 |
|
return gmp_export($this->_gmp, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Get the number as a signed integer encoded in two's complement binary. |
163
|
|
|
* |
164
|
|
|
* @return string |
165
|
|
|
*/ |
166
|
71 |
|
public function signedOctets(): string |
167
|
|
|
{ |
168
|
71 |
|
switch (gmp_sign($this->_gmp)) { |
169
|
71 |
|
case 1: |
170
|
31 |
|
return $this->_signedPositiveOctets(); |
171
|
|
|
case -1: |
172
|
33 |
|
return $this->_signedNegativeOctets(); |
173
|
|
|
} |
174
|
|
|
// zero |
175
|
7 |
|
return chr(0); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Encode positive integer in two's complement binary. |
180
|
|
|
* |
181
|
|
|
* @return string |
182
|
|
|
*/ |
183
|
31 |
|
private function _signedPositiveOctets(): string |
184
|
|
|
{ |
185
|
31 |
|
$bin = gmp_export($this->_gmp, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
186
|
|
|
// if first bit is 1, prepend full zero byte to represent positive two's complement |
187
|
31 |
|
if (ord($bin[0]) & 0x80) { |
188
|
6 |
|
$bin = chr(0x00) . $bin; |
189
|
|
|
} |
190
|
31 |
|
return $bin; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Encode negative integer in two's complement binary. |
195
|
|
|
* |
196
|
|
|
* @return string |
197
|
|
|
*/ |
198
|
33 |
|
private function _signedNegativeOctets(): string |
199
|
|
|
{ |
200
|
33 |
|
$num = gmp_abs($this->_gmp); |
201
|
|
|
// compute number of bytes required |
202
|
33 |
|
$width = 1; |
203
|
33 |
|
if ($num > 128) { |
204
|
24 |
|
$tmp = $num; |
205
|
|
|
do { |
206
|
24 |
|
++$width; |
207
|
24 |
|
$tmp >>= 8; |
208
|
24 |
|
} while ($tmp > 128); |
209
|
|
|
} |
210
|
|
|
// compute two's complement 2^n - x |
211
|
33 |
|
$num = gmp_pow('2', 8 * $width) - $num; |
212
|
33 |
|
$bin = gmp_export($num, 1, GMP_MSW_FIRST | GMP_BIG_ENDIAN); |
213
|
|
|
// if first bit is 0, prepend full inverted byte to represent negative two's complement |
214
|
33 |
|
if (!(ord($bin[0]) & 0x80)) { |
215
|
2 |
|
$bin = chr(0xff) . $bin; |
216
|
|
|
} |
217
|
33 |
|
return $bin; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Get the maximum integer value. |
222
|
|
|
* |
223
|
|
|
* @return \GMP |
224
|
|
|
*/ |
225
|
311 |
|
private function _intMaxGmp(): \GMP |
226
|
|
|
{ |
227
|
311 |
|
static $gmp; |
228
|
311 |
|
if (!isset($gmp)) { |
229
|
1 |
|
$gmp = gmp_init(PHP_INT_MAX, 10); |
230
|
|
|
} |
231
|
311 |
|
return $gmp; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Get the minimum integer value. |
236
|
|
|
* |
237
|
|
|
* @return \GMP |
238
|
|
|
*/ |
239
|
307 |
|
private function _intMinGmp(): \GMP |
240
|
|
|
{ |
241
|
307 |
|
static $gmp; |
242
|
307 |
|
if (!isset($gmp)) { |
243
|
1 |
|
$gmp = gmp_init(PHP_INT_MIN, 10); |
244
|
|
|
} |
245
|
307 |
|
return $gmp; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
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.