1: | <?php |
2: | |
3: | if (!is_callable('random_int')) { |
4: | /** |
5: | * Random_* Compatibility Library |
6: | * for using the new PHP 7 random_* API in PHP 5 projects |
7: | * |
8: | * The MIT License (MIT) |
9: | * |
10: | * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises |
11: | * |
12: | * Permission is hereby granted, free of charge, to any person obtaining a copy |
13: | * of this software and associated documentation files (the "Software"), to deal |
14: | * in the Software without restriction, including without limitation the rights |
15: | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
16: | * copies of the Software, and to permit persons to whom the Software is |
17: | * furnished to do so, subject to the following conditions: |
18: | * |
19: | * The above copyright notice and this permission notice shall be included in |
20: | * all copies or substantial portions of the Software. |
21: | * |
22: | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
23: | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
24: | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
25: | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
26: | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
27: | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
28: | * SOFTWARE. |
29: | */ |
30: | |
31: | /** |
32: | * Fetch a random integer between $min and $max inclusive |
33: | * |
34: | * @param int $min |
35: | * @param int $max |
36: | * |
37: | * @throws Exception |
38: | * |
39: | * @return int |
40: | */ |
41: | function random_int($min, $max) |
42: | { |
43: | /** |
44: | * Type and input logic checks |
45: | * |
46: | * If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX) |
47: | * (non-inclusive), it will sanely cast it to an int. If you it's equal to |
48: | * ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats |
49: | * lose precision, so the <= and => operators might accidentally let a float |
50: | * through. |
51: | */ |
52: | |
53: | try { |
54: | /** @var int $min */ |
55: | $min = RandomCompat_intval($min); |
56: | } catch (TypeError $ex) { |
57: | throw new TypeError( |
58: | 'random_int(): $min must be an integer' |
59: | ); |
60: | } |
61: | |
62: | try { |
63: | /** @var int $max */ |
64: | $max = RandomCompat_intval($max); |
65: | } catch (TypeError $ex) { |
66: | throw new TypeError( |
67: | 'random_int(): $max must be an integer' |
68: | ); |
69: | } |
70: | |
71: | /** |
72: | * Now that we've verified our weak typing system has given us an integer, |
73: | * let's validate the logic then we can move forward with generating random |
74: | * integers along a given range. |
75: | */ |
76: | if ($min > $max) { |
77: | throw new Error( |
78: | 'Minimum value must be less than or equal to the maximum value' |
79: | ); |
80: | } |
81: | |
82: | if ($max === $min) { |
83: | return (int) $min; |
84: | } |
85: | |
86: | /** |
87: | * Initialize variables to 0 |
88: | * |
89: | * We want to store: |
90: | * $bytes => the number of random bytes we need |
91: | * $mask => an integer bitmask (for use with the &) operator |
92: | * so we can minimize the number of discards |
93: | */ |
94: | $attempts = $bits = $bytes = $mask = $valueShift = 0; |
95: | /** @var int $attempts */ |
96: | /** @var int $bits */ |
97: | /** @var int $bytes */ |
98: | /** @var int $mask */ |
99: | /** @var int $valueShift */ |
100: | |
101: | /** |
102: | * At this point, $range is a positive number greater than 0. It might |
103: | * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to |
104: | * a float and we will lose some precision. |
105: | * |
106: | * @var int|float $range |
107: | */ |
108: | $range = $max - $min; |
109: | |
110: | /** |
111: | * Test for integer overflow: |
112: | */ |
113: | if (!is_int($range)) { |
114: | |
115: | /** |
116: | * Still safely calculate wider ranges. |
117: | * Provided by @CodesInChaos, @oittaa |
118: | * |
119: | * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435 |
120: | * |
121: | * We use ~0 as a mask in this case because it generates all 1s |
122: | * |
123: | * @ref https://eval.in/400356 (32-bit) |
124: | * @ref http://3v4l.org/XX9r5 (64-bit) |
125: | */ |
126: | $bytes = PHP_INT_SIZE; |
127: | /** @var int $mask */ |
128: | $mask = ~0; |
129: | |
130: | } else { |
131: | |
132: | /** |
133: | * $bits is effectively ceil(log($range, 2)) without dealing with |
134: | * type juggling |
135: | */ |
136: | while ($range > 0) { |
137: | if ($bits % 8 === 0) { |
138: | ++$bytes; |
139: | } |
140: | ++$bits; |
141: | $range >>= 1; |
142: | /** @var int $mask */ |
143: | $mask = $mask << 1 | 1; |
144: | } |
145: | $valueShift = $min; |
146: | } |
147: | |
148: | /** @var int $val */ |
149: | $val = 0; |
150: | /** |
151: | * Now that we have our parameters set up, let's begin generating |
152: | * random integers until one falls between $min and $max |
153: | */ |
154: | /** @psalm-suppress RedundantCondition */ |
155: | do { |
156: | /** |
157: | * The rejection probability is at most 0.5, so this corresponds |
158: | * to a failure probability of 2^-128 for a working RNG |
159: | */ |
160: | if ($attempts > 128) { |
161: | throw new Exception( |
162: | 'random_int: RNG is broken - too many rejections' |
163: | ); |
164: | } |
165: | |
166: | /** |
167: | * Let's grab the necessary number of random bytes |
168: | */ |
169: | $randomByteString = random_bytes($bytes); |
170: | |
171: | /** |
172: | * Let's turn $randomByteString into an integer |
173: | * |
174: | * This uses bitwise operators (<< and |) to build an integer |
175: | * out of the values extracted from ord() |
176: | * |
177: | * Example: [9F] | [6D] | [32] | [0C] => |
178: | * 159 + 27904 + 3276800 + 201326592 => |
179: | * 204631455 |
180: | */ |
181: | $val &= 0; |
182: | for ($i = 0; $i < $bytes; ++$i) { |
183: | $val |= ord($randomByteString[$i]) << ($i * 8); |
184: | } |
185: | /** @var int $val */ |
186: | |
187: | /** |
188: | * Apply mask |
189: | */ |
190: | $val &= $mask; |
191: | $val += $valueShift; |
192: | |
193: | ++$attempts; |
194: | /** |
195: | * If $val overflows to a floating point number, |
196: | * ... or is larger than $max, |
197: | * ... or smaller than $min, |
198: | * then try again. |
199: | */ |
200: | } while (!is_int($val) || $val > $max || $val < $min); |
201: | |
202: | return (int) $val; |
203: | } |
204: | } |
205: |