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