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

SubMuncher   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 260
Duplicated Lines 6.92 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 1
dl 18
loc 260
rs 8.2857
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
D consolidate() 0 31 9
C ip_range_to_subnet_array() 0 72 14
A consolidate_subnets() 9 9 2
C ultra_compression() 0 72 9
A consolidate_verbose() 0 14 2
A consolidate_subnets_verbose() 9 9 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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)
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 ($max === null || count($consolidatedSubnets) <= $max) {
45
            return $consolidatedSubnets;
46
        }
47
48
        return self::ultra_compression($consolidatedSubnets, $max);
49
    }
50
51
    /**
52
     * @param string $startip an IPv4 address
53
     * @param string $endip an IPv4 address
54
     *
55
     * @return string[] list of subnets that cover the ip range specified
56
     */
57
    public static function ip_range_to_subnet_array($startip, $endip)
58
    {
59
60
        if (!Util::is_ipaddr($startip) || !Util::is_ipaddr($endip)) {
61
            return [];
62
        }
63
64
        // Container for subnets within this range.
65
        $rangesubnets = [];
66
67
        // Figure out what the smallest subnet is that holds the number of IPs in the
68
        // given range.
69
        $cidr = Util::find_smallest_cidr(Util::ip_range_size($startip, $endip));
70
71
        // Loop here to reduce subnet size and retest as needed. We need to make sure
72
        // that the target subnet is wholly contained between $startip and $endip.
73
        for ($cidr; $cidr <= 32; $cidr++) {
74
            // Find the network and broadcast addresses for the subnet being tested.
75
            $targetsub_min = Util::gen_subnet($startip, $cidr);
76
            $targetsub_max = Util::gen_subnet_max($startip, $cidr);
77
78
            // Check best case where the range is exactly one subnet.
79
            if (($targetsub_min == $startip) && ($targetsub_max == $endip)) {
80
                // Hooray, the range is exactly this subnet!
81
                return ["{$startip}/{$cidr}"];
82
            }
83
84
            // These remaining scenarios will find a subnet that uses the largest
85
            // chunk possible of the range being tested, and leave the rest to be
86
            // tested recursively after the loop.
87
88
            // Check if the subnet begins with $startip and ends before $endip
89
            if (($targetsub_min == $startip) && Util::ip_less_than($targetsub_max, $endip)) {
90
                break;
91
            }
92
93
            // Check if the subnet ends at $endip and starts after $startip
94
            if (Util::ip_greater_than($targetsub_min, $startip) && ($targetsub_max == $endip)) {
95
                break;
96
            }
97
98
            // Check if the subnet is between $startip and $endip
99
            if (Util::ip_greater_than($targetsub_min, $startip) && Util::ip_less_than($targetsub_max, $endip)) {
100
                break;
101
            }
102
        }
103
104
        // Some logic that will recursively search from $startip to the first IP before
105
        // the start of the subnet we just found.
106
        // NOTE: This may never be hit, the way the above algo turned out, but is left
107
        // for completeness.
108
        if ($startip != $targetsub_min) {
109
            $rangesubnets = array_merge(
110
                $rangesubnets,
111
                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...
112
            );
113
        }
114
115
        // Add in the subnet we found before, to preserve ordering
116
        $rangesubnets[] = "{$targetsub_min}/{$cidr}";
117
118
        // And some more logic that will search after the subnet we found to fill in
119
        // to the end of the range.
120
        if ($endip != $targetsub_max) {
121
            $rangesubnets = array_merge(
122
                $rangesubnets,
123
                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...
124
            );
125
        }
126
127
        return $rangesubnets;
128
    }
129
130
    /**
131
     * Should be an array of CIDRS eg ['1.1.1.0/24', '2.2.2.2/31']
132
     *
133
     * @param string[] $subnetsArray
134
     * @param int $max
135
     */
136 View Code Duplication
    public static function consolidate_subnets($subnetsArray, $max = 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...
137
    {
138
        $ips = [];
139
        foreach ($subnetsArray as $subnet) {
140
            $ips = array_merge($ips, Util::cidr_to_ips_array($subnet));
141
        }
142
143
        return self::consolidate($ips, $max);
144
    }
145
146
    /**
147
     * Function to figure out the least problematic subnets to combine based on
148
     * fewest additional IPs introduced. Then combines them as such, and runs
149
     * it back through the consolidator with one less subnet - until we have
150
     * reduced it down to the maximum number of rules
151
     *
152
     * @param array $subnetsArray array of cidrs
153
     * @param int $max
154
     *
155
     * @return array
156
     */
157
    public static function ultra_compression($subnetsArray, $max = null)
158
    {
159
        $subnetToMaskMap = [];
160
        $ipReductionBySubnet = [];
161
162
        foreach ($subnetsArray as $index => $cidr) {
163
            $parts = explode('/', $cidr);
164
            $adjacentParts = [];
165
166
            if (isset($subnetsArray[$index + 1])) {
167
                $adjacentParts = explode('/', $subnetsArray[$index + 1]);
168
            }
169
170
            $subnetToMaskMap[$parts[0]] = [
171
                'mask' => $parts[1],
172
                'next' => isset($adjacentParts[0]) ? $adjacentParts[0] : 'none'
173
            ];
174
175
            if ($index == count($subnetsArray) - 1) {
176
                // we are at the end
177
                break;
178
            }
179
180
            $toJoin = Util::get_single_subnet($parts[0], Util::gen_subnet_max($adjacentParts[0], $adjacentParts[1]));
181
            if (!$toJoin) {
182
                continue;
183
            }
184
            $joinAddress = explode('/', $toJoin)[0];
185
            $joinMask = explode('/', $toJoin)[1];
186
            $diff = abs(Util::subnet_range_size($parts[1]) - Util::subnet_range_size($joinMask));
187
            $ipReductionBySubnet[$joinAddress] = [
188
                'mask' => $joinMask,
189
                'diff' => $diff,
190
                'original' => $parts[0]
191
            ];
192
        }
193
194
        // sort array by number of additional IPs introduced
195
        uasort($ipReductionBySubnet, function ($a, $b) {
196
            return $a['diff'] - $b['diff'];
197
        });
198
199
        reset($ipReductionBySubnet);
200
        $injectedIP = key($ipReductionBySubnet);
201
202
        $toUpdate = $ipReductionBySubnet[$injectedIP]['original'];
203
        $next = $subnetToMaskMap[$toUpdate]['next'];
204
205
        // remove the two subnets we've just mushed
206
        unset($subnetToMaskMap[$toUpdate]);
207
        unset($subnetToMaskMap[$next]);
208
209
        // chuck in the new one
210
        $subnetToMaskMap[$injectedIP] = [
211
            'mask' => $ipReductionBySubnet[$injectedIP]['mask'],
212
            'next' => 'none',
213
        ];
214
215
        $returnCIDRs = [];
216
        foreach ($subnetToMaskMap as $ip => $config) {
217
            $returnCIDRs[] = $ip.'/'.$config['mask'];
218
        }
219
220
        sort($returnCIDRs);
221
222
        if ($max === null || count($returnCIDRs) <= $max) {
223
            return $returnCIDRs;
224
        }
225
226
        // loop it through again to keep going until we have reached the desired number of rules
227
        return self::consolidate_subnets($returnCIDRs, $max);
228
    }
229
230
    /**
231
     * @param string[] $ipsArray
232
     * @param int|null $max
233
     * @return array
234
     */
235
    public static function consolidate_verbose($ipsArray, $max = null)
236
    {
237
        $consolidateResults = self::consolidate($ipsArray, $max);
238
        $totalIPs = [];
239
        foreach ($consolidateResults as $cidr) {
240
            $totalIPs = array_merge($totalIPs, Util::cidr_to_ips_array($cidr));
241
        }
242
243
        return [
244
            'consolidated_subnets' => $consolidateResults,
245
            'initial_IPs' => Util::sort_addresses($ipsArray),
246
            'total_IPs' => $totalIPs
247
        ];
248
    }
249
250
    /**
251
     * @param string[] $subnetsArray
252
     * @param int|null $max
253
     * @return array
254
     */
255 View Code Duplication
    public static function consolidate_subnets_verbose($subnetsArray, $max = 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...
256
    {
257
        $ips = [];
258
        foreach ($subnetsArray as $subnet) {
259
            $ips = array_merge($ips, Util::cidr_to_ips_array($subnet));
260
        }
261
262
        return self::consolidate_verbose($ips, $max);
263
    }
264
}
265