File Coverage

blib/lib/Net/Works/Network.pm
Criterion Covered Total %
statement 167 170 98.2
branch 46 52 88.4
condition 21 27 77.7
subroutine 35 37 94.5
pod 10 13 76.9
total 279 299 93.3


line stmt bran cond sub pod time code
1             package Net::Works::Network;
2              
3 3     3   1057 use strict;
  3         4  
  3         72  
4 3     3   9 use warnings;
  3         5  
  3         82  
5              
6             our $VERSION = '0.21';
7              
8 3     3   9 use Carp qw( confess );
  3         4  
  3         111  
9 3     3   833 use List::AllUtils qw( any );
  3         19987  
  3         205  
10 3     3   18 use Math::Int128 qw( uint128 );
  3         4  
  3         106  
11 3     3   11 use Net::Works::Address;
  3         4  
  3         45  
12 3     3   16 use Net::Works::Types qw( IPInt PrefixLength NetWorksAddress Str );
  3         5  
  3         117  
13             use Net::Works::Util
14 3     3   10 qw( _integer_address_to_string _string_address_to_integer );
  3         5  
  3         123  
15 3     3   13 use Scalar::Util qw( blessed );
  3         2  
  3         118  
16 3     3   11 use Socket 1.99 qw( inet_pton AF_INET AF_INET6 );
  3         44  
  3         106  
17              
18 3     3   11 use integer;
  3         3  
  3         14  
19              
20             # Using this currently breaks overloading - see
21             # https://rt.cpan.org/Ticket/Display.html?id=50938
22             #
23             #use namespace::autoclean;
24              
25             use overload (
26 3         17 q{""} => '_overloaded_as_string',
27             '<=>' => '_compare_overload',
28             'cmp' => '_compare_overload',
29 3     3   62 );
  3         7  
30              
31 3     3   169 use Moo;
  3         3  
  3         12  
32              
33             with 'Net::Works::Role::IP';
34              
35             has first => (
36             is => 'ro',
37             isa => NetWorksAddress,
38             init_arg => undef,
39             lazy => 1,
40             builder => '_build_first',
41             );
42              
43             has last => (
44             is => 'ro',
45             isa => NetWorksAddress,
46             init_arg => undef,
47             lazy => 1,
48             builder => '_build_last',
49             );
50              
51             has prefix_length => (
52             is => 'ro',
53             isa => PrefixLength,
54             required => 1,
55             );
56              
57             has _address_string => (
58             is => 'ro',
59             isa => Str,
60             init_arg => undef,
61             lazy => 1,
62             builder => '_build_address_string',
63             );
64              
65             has _subnet_integer => (
66             is => 'ro',
67             isa => IPInt,
68             init_arg => undef,
69             lazy => 1,
70             builder => '_build_subnet_integer',
71             );
72              
73             around BUILDARGS => sub {
74             my $orig = shift;
75             my $class = shift;
76              
77             my $p = $class->$orig(@_);
78             $p->{prefix_length} = delete $p->{mask_length}
79             if exists $p->{mask_length};
80              
81             return $p;
82             };
83              
84 0     0 0 0 sub mask_length { $_[0]->prefix_length() }
85              
86             sub BUILD {
87 1148     1148 0 57458 my $self = shift;
88              
89 1148         2283 $self->_validate_ip_integer();
90              
91 1147         2008 my $max = $self->bits();
92 1147 100       2417 if ( $self->prefix_length() > $max ) {
93 1         73 confess $self->prefix_length()
94             . ' is not a valid IP network prefix length';
95             }
96              
97 1146         16841 return;
98             }
99              
100             sub new_from_string {
101 782     782 1 120229 my $class = shift;
102 782         1605 my %p = @_;
103              
104 782 100       1547 die 'undef is not a valid IP network' unless defined $p{string};
105              
106 778         1898 my ( $address, $prefix_length ) = split /\//, $p{string}, 2;
107              
108 778 100       2051 my $version
    100          
109             = $p{version} ? $p{version}
110             : inet_pton( AF_INET6, $address ) ? 6
111             : 4;
112              
113 778 100 100     3100 if ( $version == 6 && inet_pton( AF_INET, $address ) ) {
114 45         46 $prefix_length += 96;
115 45         52 $address = '::' . $address;
116             }
117              
118 778         1663 my $integer = _string_address_to_integer( $address, $version );
119              
120 778 100       3831 confess "$p{string} is not a valid IP network"
121             unless defined $integer;
122              
123 760         15104 return $class->new(
124             _integer => $integer,
125             prefix_length => $prefix_length,
126             version => $version,
127             );
128             }
129              
130             sub new_from_integer {
131 410     410 1 18503 my $class = shift;
132 410         789 my %p = @_;
133              
134 410         518 my $integer = delete $p{integer};
135 410         347 my $version = delete $p{version};
136              
137 410 100 66     677 $version ||= ref $integer ? 6 : 4;
138              
139 410         7315 return $class->new(
140             _integer => $integer,
141             version => $version,
142             %p,
143             );
144             }
145              
146             sub _build_address_string {
147 728     728   5570 _integer_address_to_string( $_[0]->first_as_integer );
148             }
149              
150             sub _build_subnet_integer {
151 1112     1112   27851 my $self = shift;
152              
153 1112         2021 return $self->_prefix_length_to_mask( $self->prefix_length() );
154             }
155              
156             sub _prefix_length_to_mask {
157 13559     13559   8986 my $self = shift;
158 13559         8195 my $prefix_length = shift;
159              
160             # We need to special case 0 because left shifting a 128-bit integer by 128
161             # bits does not produce 0.
162 13559 100       24210 return $self->prefix_length() == 0
163             ? 0
164             : $self->_max()
165             & ( $self->_max() << ( $self->bits - $prefix_length ) );
166             }
167              
168             sub max_prefix_length {
169 250     250 1 1348 my $self = shift;
170              
171 250         3308 my $base = $self->first()->as_integer();
172              
173 250         4600 my $prefix_length = $self->prefix_length();
174              
175 250         434 my $bits = $self->bits;
176 250         352 while ($prefix_length) {
177 12447         11769 my $mask = $self->_prefix_length_to_mask($prefix_length);
178              
179 12447 100       26362 last if ( $base & $mask ) != $base;
180              
181 12197         15714 $prefix_length--;
182             }
183              
184 250         1122 return $prefix_length + 1;
185             }
186              
187 0     0 0 0 sub max_mask_length { $_[0]->max_prefix_length() }
188              
189             sub iterator {
190 5     5 1 883 my $self = shift;
191              
192 5         10 my $version = $self->version();
193 5         72 my $current_ip = $self->first()->as_integer();
194 5         184 my $last_ip = $self->last()->as_integer();
195              
196             return sub {
197 284 100   284   3932 return if $current_ip > $last_ip;
198              
199 279         791 Net::Works::Address->new_from_integer(
200             integer => $current_ip++,
201             version => $version,
202             );
203 5         92 };
204             }
205              
206             sub as_string {
207 779     779 1 1987 my $self = shift;
208              
209 779         13519 return join '/', lc $self->_address_string(), $self->prefix_length();
210             }
211              
212             sub _build_first {
213 1026     1026   28984 my $self = shift;
214              
215 1026         1469 my $int = $self->first_as_integer;
216              
217 1026         31424 return Net::Works::Address->new_from_integer(
218             integer => $int,
219             version => $self->version(),
220             );
221             }
222              
223 1853     1853 1 3680 sub first_as_integer { $_[0]->_integer() & $_[0]->_subnet_integer() }
224              
225             sub _build_last {
226 769     769   169574 my $self = shift;
227              
228 769         1215 my $int = $self->last_as_integer;
229              
230 769         8076 return Net::Works::Address->new_from_integer(
231             integer => $int,
232             version => $self->version(),
233             );
234             }
235              
236             sub last_as_integer {
237 1184     1184 1 1957 my $self = shift;
238              
239 1184         2076 return $self->_integer() | ( $self->_max() & ~$self->_subnet_integer() );
240             }
241              
242             sub contains {
243 46     46 1 224 my $self = shift;
244 46         34 my $thing = shift;
245              
246 46         29 my $first_integer;
247             my $last_integer;
248 46 100       146 if ( $thing->isa('Net::Works::Address') ) {
    50          
249 25         40 $first_integer = $last_integer = $thing->as_integer();
250             }
251             elsif ( $thing->isa('Net::Works::Network') ) {
252 21         26 $first_integer = $thing->first_as_integer();
253 21         313 $last_integer = $thing->last_as_integer();
254             }
255             else {
256 0         0 confess
257             "$thing is not a Net::Works::Address or Net::Works::Network object";
258             }
259              
260 46   100     507 return $first_integer >= $self->first_as_integer()
261             && $last_integer <= $self->last_as_integer();
262             }
263              
264             ## no critic (Subroutines::ProhibitBuiltinHomonyms)
265             sub split {
266 34     34 1 180 my $self = shift;
267              
268 34 100       54 return () if $self->prefix_length() == $self->bits();
269              
270 32         41 my $first_int = $self->first_as_integer();
271 32         731 my $last_int = $self->last_as_integer();
272              
273             return (
274 32         201 Net::Works::Network->new_from_integer(
275             integer => $first_int,
276             prefix_length => $self->prefix_length() + 1,
277             ),
278             Net::Works::Network->new_from_integer(
279             integer => ( $first_int + ( ( $last_int - $first_int ) / 2 ) )
280             + 1,
281             prefix_length => $self->prefix_length() + 1,
282             )
283             );
284             }
285             ## use critic
286              
287             sub range_as_subnets {
288 5     5 1 128 my $class = shift;
289 5         9 my $first_ip = shift;
290 5         6 my $last_ip = shift;
291 5 100 100 7   45 my $version = shift || ( any { /:/ } $first_ip, $last_ip ) ? 6 : 4;
  7         27  
292              
293 5 50       35 $first_ip = Net::Works::Address->new_from_string(
294             string => $first_ip,
295             version => $version,
296             ) unless ref $first_ip;
297              
298 5 50       42 $last_ip = Net::Works::Address->new_from_string(
299             string => $last_ip,
300             version => $version,
301             ) unless ref $last_ip;
302              
303 5         33 my @ranges = $class->_remove_reserved_subnets_from_range(
304             $first_ip->as_integer(),
305             $last_ip->as_integer(),
306             $version
307             );
308              
309 5         10 my @subnets;
310 5         8 for my $range (@ranges) {
311 24         27 push @subnets, $class->_split_one_range( @{$range}, $version );
  24         66  
312             }
313              
314 5         96 return @subnets;
315             }
316              
317             {
318             my @reserved_4 = qw(
319             0.0.0.0/8
320             10.0.0.0/8
321             100.64.0.0/10
322             127.0.0.0/8
323             169.254.0.0/16
324             172.16.0.0/12
325             192.0.0.0/29
326             192.0.2.0/24
327             192.88.99.0/24
328             192.168.0.0/16
329             198.18.0.0/15
330             198.51.100.0/24
331             203.0.113.0/24
332             224.0.0.0/4
333             240.0.0.0/4
334             );
335              
336             # ::/128 and ::1/128 are reserved under IPv6 but these are already covered
337             # under 0.0.0.0/8
338             my @reserved_6 = (
339             @reserved_4, qw(
340             100::/64
341             2001::/23
342             2001:db8::/32
343             fc00::/7
344             fe80::/10
345             ff00::/8
346             )
347             );
348              
349             my %reserved_networks = (
350             4 => [
351             map { [ $_->first()->as_integer(), $_->last()->as_integer() ] }
352             sort { $a->first <=> $b->first }
353             map {
354             Net::Works::Network->new_from_string(
355             string => $_,
356             version => 4
357             )
358             } @reserved_4,
359             ],
360             6 => [
361             map { [ $_->first()->as_integer(), $_->last()->as_integer() ] }
362             sort { $a->first <=> $b->first }
363             map {
364             Net::Works::Network->new_from_string(
365             string => $_,
366             version => 6
367             )
368             } @reserved_6,
369             ],
370             );
371              
372             sub _remove_reserved_subnets_from_range {
373 13     13   265 my $class = shift;
374 13         15 my $first_ip = shift;
375 13         15 my $last_ip = shift;
376 13         13 my $version = shift;
377              
378 13         14 my @ranges;
379 13         9 my $add_remaining = 1;
380              
381 13         15 for my $pn ( @{ $reserved_networks{$version} } ) {
  13         36  
382 82         78 my $reserved_first = $pn->[0];
383 82         66 my $reserved_last = $pn->[1];
384              
385 82 100       174 next if ( $reserved_last <= $first_ip );
386 74 100       124 last if ( $last_ip < $reserved_first );
387              
388 66 100       153 push @ranges, [ $first_ip, $reserved_first - 1 ]
389             if $first_ip < $reserved_first;
390              
391 66 100       109 if ( $last_ip <= $reserved_last ) {
392 5         8 $add_remaining = 0;
393 5         7 last;
394             }
395              
396 61         94 $first_ip = $reserved_last + 1;
397             }
398              
399 13 100       33 push @ranges, [ $first_ip, $last_ip ] if $add_remaining;
400              
401 13         29 return @ranges;
402             }
403             }
404              
405             sub _split_one_range {
406 24     24   24 my $class = shift;
407 24         29 my $first_ip = shift;
408 24         23 my $last_ip = shift;
409 24         25 my $version = shift;
410              
411 24         24 my @subnets;
412 24         43 while ( $first_ip <= $last_ip ) {
413 326         6430 my $max_network = _max_subnet( $first_ip, $last_ip, $version );
414              
415 326         1518 push @subnets, $max_network;
416              
417 326         431 $first_ip = $max_network->last_as_integer + 1;
418             }
419              
420 24         652 return @subnets;
421             }
422              
423             sub _max_subnet {
424 326     326   267 my $ip = shift;
425 326         303 my $maxip = shift;
426 326         248 my $version = shift;
427              
428 326 100       390 my $prefix_length = $version == 6 ? 128 : 32;
429              
430 326         289 my $v = $ip;
431 326 100       626 my $reverse_mask = $version == 6 ? uint128(1) : 1;
432              
433 326   66     1948 while (( $v & 1 ) == 0
      100        
434             && $prefix_length > 0
435             && ( $ip | $reverse_mask ) <= $maxip ) {
436              
437 19009         14154 $prefix_length--;
438 19009         18542 $v = $v >> 1;
439              
440 19009         106793 $reverse_mask = ( $reverse_mask << 1 ) | 1;
441             }
442              
443 326         727 return Net::Works::Network->new_from_integer(
444             integer => $ip,
445             prefix_length => $prefix_length,
446             version => $version,
447             );
448             }
449              
450             sub _compare_overload {
451 30     30   1691 my $self = shift;
452 30         19 my $other = shift;
453              
454             confess 'Cannot compare unless both objects are '
455             . __PACKAGE__
456             . ' objects'
457             unless blessed $self
458             && blessed $other
459 30 50 33     133 && eval { $self->isa(__PACKAGE__) && $other->isa(__PACKAGE__) };
  30 50 33     137  
460              
461 30   100     398 my $cmp = (
462             $self->first() <=> $other->first()
463             or $self->prefix_length() <=> $other->prefix_length()
464             );
465              
466 30 50       540 return shift() ? $cmp * -1 : $cmp;
467             }
468              
469             __PACKAGE__->meta()->make_immutable();
470              
471             1;
472              
473             # ABSTRACT: An object representing a single IP address (4 or 6) subnet
474              
475             __END__