1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Samsara\Fermat\Core\Provider; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use Random\RandomException; |
7
|
|
|
use Samsara\Exceptions\SystemError\PlatformError\MissingPackage; |
8
|
|
|
use Samsara\Exceptions\UsageError\IntegrityConstraint; |
9
|
|
|
use Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState; |
10
|
|
|
use Samsara\Exceptions\UsageError\OptionalExit; |
11
|
|
|
use Samsara\Fermat\Core\Enums\NumberBase; |
12
|
|
|
use Samsara\Fermat\Core\Enums\RandomMode; |
13
|
|
|
use Samsara\Fermat\Core\Numbers; |
14
|
|
|
use Samsara\Fermat\Core\Types\Base\Interfaces\Numbers\DecimalInterface; |
15
|
|
|
use Samsara\Fermat\Core\Values\ImmutableDecimal; |
16
|
|
|
use Random\Randomizer; |
17
|
|
|
use Random\Engine\Secure; |
18
|
|
|
use Random\Engine\PcgOneseq128XslRr64; |
19
|
|
|
use Random\Engine\Xoshiro256StarStar; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* |
23
|
|
|
*/ |
24
|
|
|
class RandomProvider |
25
|
|
|
{ |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @param int|string|DecimalInterface $min |
29
|
|
|
* @param int|string|DecimalInterface $max |
30
|
|
|
* @param RandomMode $mode |
31
|
|
|
* @param int|null $seed |
32
|
|
|
* @return ImmutableDecimal |
33
|
|
|
* @throws RandomException |
34
|
|
|
* @throws IntegrityConstraint |
35
|
|
|
*/ |
36
|
14 |
|
public static function randomInt( |
37
|
|
|
int|string|DecimalInterface $min, |
38
|
|
|
int|string|DecimalInterface $max, |
39
|
|
|
RandomMode $mode = RandomMode::Entropy, |
40
|
|
|
?int $seed = null |
41
|
|
|
): ImmutableDecimal |
42
|
|
|
{ |
43
|
14 |
|
$minDecimal = Numbers::makeOrDont(Numbers::IMMUTABLE, $min); |
44
|
14 |
|
$maxDecimal = Numbers::makeOrDont(Numbers::IMMUTABLE, $max); |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* We want to prevent providing non-integer values for min and max, even in cases where |
48
|
|
|
* the supplied value is a string or a DecimalInterface. |
49
|
|
|
*/ |
50
|
14 |
|
if ($minDecimal->isFloat() || $maxDecimal->isFloat()) { |
51
|
|
|
throw new IntegrityConstraint( |
52
|
|
|
'Random integers cannot be generated with boundaries which are floats', |
53
|
|
|
'Provide only whole number, integer values for min and max.', |
54
|
|
|
'An attempt was made to generate a random integer with boundaries which are non-integers. Min Provided: '.$min->getValue(NumberBase::Ten).' -- Max Provided: '.$max->getValue(NumberBase::Ten) |
|
|
|
|
55
|
|
|
); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Because of optimistic optimizing with the rand() and random_int() functions, we do |
60
|
|
|
* need the arguments to be provided in the correct order. |
61
|
|
|
*/ |
62
|
14 |
|
if ($minDecimal->isGreaterThan($maxDecimal)) { |
63
|
|
|
$tempDecimal = $minDecimal; |
64
|
|
|
$minDecimal = $maxDecimal; |
65
|
|
|
$maxDecimal = $tempDecimal; |
66
|
|
|
unset($tempDecimal); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* For some applications it might be better to throw an exception here, however it |
71
|
|
|
* would probably be hard to recover in most applications from a situation which |
72
|
|
|
* resulted in this situation. |
73
|
|
|
* |
74
|
|
|
* So instead we will trigger a language level warning and return the only valid |
75
|
|
|
* value for the parameters given. |
76
|
|
|
*/ |
77
|
14 |
|
if ($minDecimal->isEqual($maxDecimal)) { |
78
|
2 |
|
trigger_error( |
79
|
2 |
|
'Attempted to get a random value for a range of no size, with minimum of '.$minDecimal->getValue(NumberBase::Ten).' and maximum of '.$maxDecimal->getValue(NumberBase::Ten), |
80
|
|
|
E_USER_WARNING |
81
|
|
|
); |
82
|
|
|
|
83
|
|
|
return $minDecimal; |
84
|
|
|
} |
85
|
|
|
|
86
|
12 |
|
if ($minDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $minDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { |
87
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */ |
88
|
12 |
|
$min = $minDecimal->asInt(); |
89
|
|
|
} |
90
|
|
|
|
91
|
12 |
|
if ($maxDecimal->isLessThanOrEqualTo(PHP_INT_MAX) && $maxDecimal->isGreaterThanOrEqualTo(PHP_INT_MIN)) { |
92
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */ |
93
|
10 |
|
$max = $maxDecimal->asInt(); |
94
|
|
|
} |
95
|
|
|
|
96
|
12 |
|
$randomizer = self::getRandomizer($mode, $seed); |
97
|
|
|
|
98
|
12 |
|
if (is_int($min) && is_int($max)) { |
99
|
10 |
|
$num = $randomizer->getInt($min, $max); |
100
|
10 |
|
return new ImmutableDecimal($num); |
101
|
|
|
} else { |
102
|
|
|
/** |
103
|
|
|
* We only need to request enough bytes to find a number within the range, since we |
104
|
|
|
* will be adding the minimum value to it at the end. |
105
|
|
|
*/ |
106
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */ |
107
|
2 |
|
$range = $maxDecimal->subtract($minDecimal); |
108
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */ |
109
|
2 |
|
$bitsNeeded = $range->ln(2)->divide(Numbers::makeNaturalLog2(2), 2)->floor()->add(1); |
110
|
2 |
|
$bytesNeeded = $bitsNeeded->divide(8, 2)->ceil(); |
111
|
|
|
|
112
|
|
|
do { |
113
|
|
|
try { |
114
|
|
|
/** |
115
|
|
|
* Returns random bytes based on sources of entropy within the system. |
116
|
|
|
* |
117
|
|
|
* For documentation on these sources please see: |
118
|
|
|
* |
119
|
|
|
* https://www.php.net/manual/en/function.random-bytes.php |
120
|
|
|
*/ |
121
|
2 |
|
$entropyBytes = $randomizer->getBytes($bytesNeeded->asInt()); |
122
|
2 |
|
$baseTwoBytes = ''; |
123
|
2 |
|
for($i = 0; $i < strlen($entropyBytes); $i++){ |
124
|
2 |
|
$baseTwoBytes .= decbin( ord( $entropyBytes[$i] ) ); |
125
|
|
|
} |
126
|
|
|
} catch (Exception $e) { |
127
|
|
|
throw new OptionalExit( |
128
|
|
|
'System error from random_bytes().', |
129
|
|
|
'Ensure your system is configured correctly.', |
130
|
|
|
'A call to random_bytes() threw a system level exception. Most often this is due to a problem with entropy sources in your configuration. Original exception message: ' . $e->getMessage() |
131
|
|
|
); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Since the number of digits is equal to the bits needed, but random_bytes() only |
136
|
|
|
* returns in chunks of 8 bits (duh, bytes), we can substr() from the right to |
137
|
|
|
* select only the correct number of digits by multiplying the number of bits |
138
|
|
|
* needed by -1 and using that as the starting point. |
139
|
|
|
*/ |
140
|
2 |
|
$randomValue = BaseConversionProvider::convertStringToBaseTen( |
141
|
2 |
|
substr($baseTwoBytes, $bitsNeeded->multiply(-1)->asInt()), |
142
|
|
|
NumberBase::Two |
143
|
|
|
); |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @var ImmutableDecimal $num |
147
|
|
|
*/ |
148
|
2 |
|
$num = Numbers::make( |
149
|
|
|
type: Numbers::IMMUTABLE, |
150
|
|
|
value: $randomValue |
151
|
|
|
); |
152
|
2 |
|
} while ($num->isGreaterThan($range)); |
153
|
|
|
/** |
154
|
|
|
* It is strictly speaking possible for this to loop infinitely. In the worst case |
155
|
|
|
* scenario where 50% of possible values are invalid, it takes 7 loops for there to |
156
|
|
|
* be a less than a 1% chance of still not having an answer. |
157
|
|
|
* |
158
|
|
|
* After only 10 loops the chance is less than 1/1000. |
159
|
|
|
* |
160
|
|
|
* NOTE: Worst case scenario is a range of size 2^n + 1. |
161
|
|
|
*/ |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Add the minimum since we effectively subtracted it by finding a random number |
165
|
|
|
* bounded between 0 and range. If our requested range included negative numbers, |
166
|
|
|
* this operation will also return those values into our data by effectively |
167
|
|
|
* shifting the result window. |
168
|
|
|
*/ |
169
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */ |
170
|
2 |
|
return $num->add($minDecimal); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @param int $scale |
176
|
|
|
* @param RandomMode $mode |
177
|
|
|
* @return ImmutableDecimal |
178
|
|
|
* @throws RandomException |
179
|
|
|
*/ |
180
|
8 |
|
public static function randomDecimal( |
181
|
|
|
int $scale = 10, |
182
|
|
|
RandomMode $mode = RandomMode::Entropy |
183
|
|
|
): ImmutableDecimal |
184
|
|
|
{ |
185
|
8 |
|
$randomizer = self::getRandomizer($mode); |
186
|
|
|
|
187
|
8 |
|
$result = '0.'; |
188
|
8 |
|
for ($i = 0; $i < $scale; $i++) { |
189
|
8 |
|
$result .= $randomizer->getInt(0, 9); |
190
|
|
|
} |
191
|
|
|
|
192
|
8 |
|
return new ImmutableDecimal($result, $scale); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @param int|string|DecimalInterface $min |
197
|
|
|
* @param int|string|DecimalInterface $max |
198
|
|
|
* @param int $scale |
199
|
|
|
* @param RandomMode $mode |
200
|
|
|
* @return ImmutableDecimal |
201
|
|
|
* @throws RandomException |
202
|
|
|
*/ |
203
|
6 |
|
public static function randomReal( |
204
|
|
|
int|string|DecimalInterface $min, |
205
|
|
|
int|string|DecimalInterface $max, |
206
|
|
|
int $scale, |
207
|
|
|
RandomMode $mode = RandomMode::Entropy |
208
|
|
|
): ImmutableDecimal |
209
|
|
|
{ |
210
|
6 |
|
$min = new ImmutableDecimal($min); |
211
|
6 |
|
$max = new ImmutableDecimal($max); |
212
|
|
|
|
213
|
6 |
|
if ($min->isEqual($max)) { |
214
|
|
|
trigger_error( |
215
|
|
|
'Attempted to get a random value for a range of no size, with minimum of '.$min->getValue(NumberBase::Ten).' and maximum of '.$max->getValue(NumberBase::Ten), |
216
|
|
|
E_USER_WARNING |
217
|
|
|
); |
218
|
|
|
|
219
|
|
|
return $min; |
220
|
|
|
} |
221
|
|
|
|
222
|
6 |
|
$intScale = $scale + 2; |
223
|
|
|
|
224
|
6 |
|
$range = $max->subtract($min); |
225
|
|
|
|
226
|
6 |
|
$intScale = $intScale + $range->numberOfTotalDigits(); |
227
|
|
|
|
228
|
6 |
|
$randomDecimal = self::randomDecimal($intScale, $mode); |
229
|
|
|
|
230
|
6 |
|
return $randomDecimal->multiply($range)->add($min)->truncateToScale($scale); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param RandomMode $mode |
235
|
|
|
* @param int|null $seed |
236
|
|
|
* @return Randomizer |
237
|
|
|
* @throws RandomException |
238
|
|
|
*/ |
239
|
20 |
|
private static function getRandomizer(RandomMode $mode, ?int $seed = null): Randomizer |
240
|
|
|
{ |
241
|
20 |
|
$engine = match ($mode) { |
242
|
20 |
|
RandomMode::Speed => new Xoshiro256StarStar($seed), |
243
|
16 |
|
RandomMode::Entropy => new PcgOneseq128XslRr64($seed), |
244
|
|
|
RandomMode::Secure => new Secure(), |
245
|
|
|
}; |
246
|
|
|
|
247
|
20 |
|
return new Randomizer($engine); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
} |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.