Passed
Push — master ( 6ebcbe...15baa9 )
by Andrew
02:31
created

SubMuncher   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 1
dl 0
loc 242
rs 8.8
c 0
b 0
f 0

5 Methods

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