Test Failed
Pull Request — master (#6)
by Andrew
02:41
created

SubMuncher::consolidate()   D

Complexity

Conditions 10
Paths 20

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 22
nc 20
nop 3

How to fix   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
namespace AndrewAndante\SubMuncher;
4
5
class SubMuncher
6
{
7
    /**
8
     * This class should not be instantiated.
9
     */
10
    private function __construct()
11
    {
12
    }
13
14
    /**
15
     * @param array $ipsArray
16
     * @param int $max max number of rules returned
17
     * @return array
18
     */
19
    public static function consolidate($ipsArray, $max = null, $alreadyProcessed = null)
20
    {
21
        $consolidatedSubnets = [];
22
        $subnetStart = null;
23
24
        $ips = array_unique($ipsArray);
25
        $sortedIPs = Util::sort_addresses($ips);
26
27
        foreach ($sortedIPs as $index => $ipv4) {
28
            // If not last and the next IP is the next sequential one, we are at the beginning of a subnet
29
            if (isset($sortedIPs[$index + 1]) && $sortedIPs[$index + 1] == Util::ip_after($ipv4)) {
30
                // if we've already started, just keep going, else kick one off
31
                $subnetStart = $subnetStart ?: $ipv4;
32
                // if not the first IP and the previous IP is sequential, we're at the end of a subnet
33
            } elseif (isset($sortedIPs[$index - 1]) && $subnetStart !== null) {
34
                $result = self::ip_range_to_subnet_array($subnetStart, $ipv4);
35
                $consolidatedSubnets = array_merge($consolidatedSubnets, $result);
36
                $subnetStart = null;
37
                // otherwise we are a lone /32, so add it straight in
38
            } else {
39
                $consolidatedSubnets[]= $ipv4.'/32';
40
                $subnetStart = null;
41
            }
42
        }
43
44
        if ($alreadyProcessed !== null) {
45
            $totalSubnets = array_merge($consolidatedSubnets, $alreadyProcessed);
46
        } else {
47
            $totalSubnets = $consolidatedSubnets;
48
        }
49
50
        if ($max === null || count($totalSubnets) <= $max) {
51
            return $totalSubnets;
52
        }
53
54
        return self::ultra_compression($consolidatedSubnets, $max, $alreadyProcessed);
55
    }
56
57
    /**
58
     * @param string $startip an IPv4 address
59
     * @param string $endip an IPv4 address
60
     *
61
     * @return string[] list of subnets that cover the ip range specified
62
     */
63
    public static function ip_range_to_subnet_array($startip, $endip)
64
    {
65
66
        if (!Util::is_ipaddr($startip) || !Util::is_ipaddr($endip)) {
67
            return [];
68
        }
69
70
        // Container for subnets within this range.
71
        $rangesubnets = [];
72
73
        // Figure out what the smallest subnet is that holds the number of IPs in the
74
        // given range.
75
        $cidr = Util::find_smallest_cidr(Util::ip_range_size($startip, $endip));
76
77
        // Loop here to reduce subnet size and retest as needed. We need to make sure
78
        // that the target subnet is wholly contained between $startip and $endip.
79
        for ($cidr; $cidr <= 32; $cidr++) {
80
            // Find the network and broadcast addresses for the subnet being tested.
81
            $targetsub_min = Util::gen_subnet($startip, $cidr);
82
            $targetsub_max = Util::gen_subnet_max($startip, $cidr);
83
84
            // Check best case where the range is exactly one subnet.
85
            if (($targetsub_min == $startip) && ($targetsub_max == $endip)) {
86
                // Hooray, the range is exactly this subnet!
87
                return ["{$startip}/{$cidr}"];
88
            }
89
90
            // These remaining scenarios will find a subnet that uses the largest
91
            // chunk possible of the range being tested, and leave the rest to be
92
            // tested recursively after the loop.
93
94
            // Check if the subnet begins with $startip and ends before $endip
95
            if (($targetsub_min == $startip) && Util::ip_less_than($targetsub_max, $endip)) {
96
                break;
97
            }
98
99
            // Check if the subnet ends at $endip and starts after $startip
100
            if (Util::ip_greater_than($targetsub_min, $startip) && ($targetsub_max == $endip)) {
101
                break;
102
            }
103
104
            // Check if the subnet is between $startip and $endip
105
            if (Util::ip_greater_than($targetsub_min, $startip) && Util::ip_less_than($targetsub_max, $endip)) {
106
                break;
107
            }
108
        }
109
110
        // Some logic that will recursively search from $startip to the first IP before
111
        // the start of the subnet we just found.
112
        // NOTE: This may never be hit, the way the above algo turned out, but is left
113
        // for completeness.
114
        if ($startip != $targetsub_min) {
115
            $rangesubnets = array_merge(
116
                $rangesubnets,
117
                self::ip_range_to_subnet_array($startip, Util::ip_before($targetsub_min))
0 ignored issues
show
Bug introduced by
The variable $targetsub_min does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
118
            );
119
        }
120
121
        // Add in the subnet we found before, to preserve ordering
122
        $rangesubnets[] = "{$targetsub_min}/{$cidr}";
123
124
        // And some more logic that will search after the subnet we found to fill in
125
        // to the end of the range.
126
        if ($endip != $targetsub_max) {
127
            $rangesubnets = array_merge(
128
                $rangesubnets,
129
                self::ip_range_to_subnet_array(Util::ip_after($targetsub_max), $endip)
0 ignored issues
show
Bug introduced by
The variable $targetsub_max does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
130
            );
131
        }
132
133
        return $rangesubnets;
134
    }
135
136
    /**
137
     * Should be an array of CIDRS eg ['1.1.1.0/24', '2.2.2.2/31']
138
     *
139
     * @param string[] $subnetsArray
140
     * @param int $max
141
     */
142 View Code Duplication
    public static function consolidate_subnets($subnetsArray, $max = null, $alreadyProcessed = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
143
    {
144
        $ips = [];
145
        foreach ($subnetsArray as $subnet) {
146
            $ips = array_merge($ips, Util::cidr_to_ips_array($subnet));
147
        }
148
149
        return self::consolidate($ips, $max, $alreadyProcessed);
150
    }
151
152
    /**
153
     * Function to figure out the least problematic subnets to combine based on
154
     * fewest additional IPs introduced. Then combines them as such, and runs
155
     * it back through the consolidator with one less subnet - until we have
156
     * reduced it down to the maximum number of rules
157
     *
158
     * @param array $subnetsArray array of cidrs
159
     * @param int $max
160
     *
161
     * @return array
162
     */
163
    public static function ultra_compression($subnetsArray, $max = null, $alreadyProcessed = null)
164
    {
165
        $subnetToMaskMap = [];
166
        $ipReductionBySubnet = [];
167
168
        foreach ($subnetsArray as $index => $cidr) {
169
            $parts = explode('/', $cidr);
170
            $adjacentParts = [];
171
172
            if (isset($subnetsArray[$index + 1])) {
173
                $adjacentParts = explode('/', $subnetsArray[$index + 1]);
174
            }
175
176
            $subnetToMaskMap[$parts[0]] = [
177
                'mask' => $parts[1],
178
                'next' => isset($adjacentParts[0]) ? $adjacentParts[0] : 'none'
179
            ];
180
181
            if ($index == count($subnetsArray) - 1) {
182
                // we are at the end
183
                break;
184
            }
185
186
            $toJoin = Util::get_single_subnet($parts[0], Util::gen_subnet_max($adjacentParts[0], $adjacentParts[1]));
187
            if (!$toJoin) {
188
                continue;
189
            }
190
            $joinAddress = explode('/', $toJoin)[0];
191
            $joinMask = explode('/', $toJoin)[1];
192
            $diff = abs(Util::subnet_range_size($parts[1]) - Util::subnet_range_size($joinMask));
193
            $ipReductionBySubnet[$joinAddress] = [
194
                'mask' => $joinMask,
195
                'diff' => $diff,
196
                'original' => $parts[0]
197
            ];
198
        }
199
200
        // sort array by number of additional IPs introduced
201
        uasort($ipReductionBySubnet, function ($a, $b) {
202
            return $a['diff'] - $b['diff'];
203
        });
204
205
        reset($ipReductionBySubnet);
206
        $injectedIP = key($ipReductionBySubnet);
207
208
        $toUpdate = $ipReductionBySubnet[$injectedIP]['original'];
209
        $next = $subnetToMaskMap[$toUpdate]['next'];
210
211
        // remove the two subnets we've just mushed
212
        unset($subnetToMaskMap[$toUpdate]);
213
        unset($subnetToMaskMap[$next]);
214
215
        // chuck in the new one
216
        $alreadyProcessed[] = $injectedIP . '/' . $ipReductionBySubnet[$injectedIP]['mask'];
217
218
        $returnCIDRs = [];
219
        foreach ($subnetToMaskMap as $ip => $config) {
220
            $returnCIDRs[] = $ip.'/'.$config['mask'];
221
        }
222
223
224
        if ($alreadyProcessed !== null) {
225
            $totalSubnets = array_merge($returnCIDRs, $alreadyProcessed);
226
        } else {
227
            $totalSubnets = $returnCIDRs;
228
        }
229
230
        sort($totalSubnets);
231
232
        if ($max === null || count($totalSubnets) <= $max) {
233
            return $totalSubnets;
234
        }
235
236
        // loop it through again to keep going until we have reached the desired number of rules
237
        return self::consolidate_subnets($returnCIDRs, $max, $alreadyProcessed);
238
    }
239
240
    /**
241
     * @param string[] $ipsArray
242
     * @param int|null $max
243
     * @return array
244
     */
245
    public static function consolidate_verbose($ipsArray, $max = null, $alreadyProcessed = null)
246
    {
247
        $consolidateResults = self::consolidate($ipsArray, $max, $alreadyProcessed);
248
        $totalIPs = [];
249
        foreach ($consolidateResults as $cidr) {
250
            $totalIPs = array_merge($totalIPs, Util::cidr_to_ips_array($cidr));
251
        }
252
253
        return [
254
            'consolidated_subnets' => $consolidateResults,
255
            'initial_IPs' => Util::sort_addresses($ipsArray),
256
            'total_IPs' => $totalIPs
257
        ];
258
    }
259
260
    /**
261
     * @param string[] $subnetsArray
262
     * @param int|null $max
263
     * @return array
264
     */
265 View Code Duplication
    public static function consolidate_subnets_verbose($subnetsArray, $max = null, $alreadyProcessed = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
    {
267
        $ips = [];
268
        foreach ($subnetsArray as $subnet) {
269
            $ips = array_merge($ips, Util::cidr_to_ips_array($subnet));
270
        }
271
272
        return self::consolidate_verbose($ips, $max, $alreadyProcessed);
273
    }
274
}
275