Completed
Pull Request — release-2.1 (#5077)
by Mathias
06:39
created

random_int.php ➔ random_int()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 163

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 10
nop 2
dl 0
loc 163
rs 5.2933
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
0 ignored issues
show
Bug introduced by
The class TypeError does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The class TypeError does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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