Completed
Pull Request — master (#74)
by
unknown
04:47
created

Paragonie_RandomInt::random_int()   D

Complexity

Conditions 13
Paths 32

Size

Total Lines 130
Code Lines 34

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 130
rs 4.9923
cc 13
eloc 34
nc 32
nop 2

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
 * Random_* Compatibility Library 
4
 * for using the new PHP 7 random_* API in PHP 5 projects
5
 * 
6
 * The MIT License (MIT)
7
 * 
8
 * Copyright (c) 2015 Paragon Initiative Enterprises
9
 * 
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy
11
 * of this software and associated documentation files (the "Software"), to deal
12
 * in the Software without restriction, including without limitation the rights
13
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
 * copies of the Software, and to permit persons to whom the Software is
15
 * furnished to do so, subject to the following conditions:
16
 * 
17
 * The above copyright notice and this permission notice shall be included in
18
 * all copies or substantial portions of the Software.
19
 * 
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
 * SOFTWARE.
27
 */
28
29
class Paragonie_RandomInt
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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
    public static 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
        is_int($min) or $min = Paragonie_Util_Intval::intval($min, __FUNCTION__, 1);
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
54
        is_int($max) or $max = Paragonie_Util_Intval::intval($max, __FUNCTION__, 2);
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
55
        
56
        /**
57
         * Now that we've verified our weak typing system has given us an integer,
58
         * let's validate the logic then we can move forward with generating random
59
         * integers along a given range.
60
         */
61
        if ($min > $max) {
62
            throw new Error(
63
                'Minimum value must be less than or equal to the maximum value'
64
            );
65
        }
66
        if ($max === $min) {
67
            return $min;
68
        }
69
70
        /**
71
         * Initialize variables to 0
72
         * 
73
         * We want to store:
74
         * $bytes => the number of random bytes we need
75
         * $mask => an integer bitmask (for use with the &) operator
76
         *          so we can minimize the number of discards
77
         */
78
        $attempts = $bits = $bytes = $mask = $valueShift = 0;
79
80
        /**
81
         * At this point, $range is a positive number greater than 0. It might
82
         * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to
83
         * a float and we will lose some precision.
84
         */
85
        $range = $max - $min;
86
87
        /**
88
         * Test for integer overflow:
89
         */
90
        if (!is_int($range)) {
91
            /**
92
             * Still safely calculate wider ranges.
93
             * Provided by @CodesInChaos, @oittaa
94
             * 
95
             * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435
96
             * 
97
             * We use ~0 as a mask in this case because it generates all 1s
98
             * 
99
             * @ref https://eval.in/400356 (32-bit)
100
             * @ref http://3v4l.org/XX9r5  (64-bit)
101
             */
102
            $bytes = PHP_INT_SIZE;
103
            $mask = ~0;
104
        } else {
105
            /**
106
             * $bits is effectively ceil(log($range, 2)) without dealing with 
107
             * type juggling
108
             */
109
            while ($range > 0) {
110
                if ($bits % 8 === 0) {
111
                   ++$bytes;
112
                }
113
                ++$bits;
114
                $range >>= 1;
115
                $mask = $mask << 1 | 1;
116
            }
117
            $valueShift = $min;
118
        }
119
120
        /**
121
         * Now that we have our parameters set up, let's begin generating
122
         * random integers until one falls between $min and $max
123
         */
124
        do {
125
            /**
126
             * The rejection probability is at most 0.5, so this corresponds
127
             * to a failure probability of 2^-128 for a working RNG
128
             */
129
            if ($attempts > 128) {
130
                throw new Exception(
131
                    'Could not gather sufficient random data'
132
                );
133
            }
134
135
            /**
136
             * Let's grab the necessary number of random bytes
137
             */
138
            $randomByteString = random_bytes($bytes);
139
140
            /**
141
             * Let's turn $randomByteString into an integer
142
             * 
143
             * This uses bitwise operators (<< and |) to build an integer
144
             * out of the values extracted from ord()
145
             * 
146
             * Example: [9F] | [6D] | [32] | [0C] =>
147
             *   159 + 27904 + 3276800 + 201326592 =>
148
             *   204631455
149
             */
150
            $val = 0;
151
            for ($i = 0; $i < $bytes; ++$i) {
152
                $val |= ord($randomByteString[$i]) << ($i * 8);
153
            }
154
155
            /**
156
             * Apply mask
157
             */
158
            $val &= $mask;
159
            $val += $valueShift;
160
161
            ++$attempts;
162
            /**
163
             * If $val overflows to a floating point number,
164
             * ... or is larger than $max,
165
             * ... or smaller than $min,
166
             * then try again.
167
             */
168
        } while (!is_int($val) || $val > $max || $val < $min);
169
        return (int) $val;
170
    }
171
}
172