| 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: |