Passed
Pull Request — master (#6)
by Andrew
02:29
created

SubMuncher   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 259
Duplicated Lines 6.95 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

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

7 Methods

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