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: