File Coverage

blib/lib/Net/Works/Network.pm
Criterion Covered Total %
statement 169 172 98.2
branch 46 52 88.4
condition 23 30 76.6
subroutine 36 38 94.7
pod 11 14 78.5
total 285 306 93.1


line stmt bran cond sub pod time code
1             package Net::Works::Network;
2              
3 3     3   1380 use strict;
  3         4  
  3         83  
4 3     3   11 use warnings;
  3         4  
  3         102  
5              
6             our $VERSION = '0.22';
7              
8 3     3   12 use Carp qw( confess );
  3         3  
  3         140  
9 3     3   1239 use List::AllUtils qw( any );
  3         14735  
  3         171  
10 3     3   14 use Math::Int128 qw( uint128 );
  3         4  
  3         101  
11 3     3   10 use Net::Works::Address;
  3         3  
  3         53  
12 3     3   10 use Net::Works::Types qw( IPInt PrefixLength NetWorksAddress Str );
  3         3  
  3         129  
13             use Net::Works::Util
14 3     3   11 qw( _integer_address_to_string _string_address_to_integer );
  3         4  
  3         104  
15 3     3   13 use Scalar::Util qw( blessed );
  3         8  
  3         106  
16 3     3   9 use Socket 1.99 qw( inet_pton AF_INET AF_INET6 );
  3         40  
  3         120  
17              
18 3     3   11 use integer;
  3         3  
  3         15  
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         19 q{""} => '_overloaded_as_string',
27             '<=>' => '_compare_overload',
28             'cmp' => '_compare_overload',
29 3     3   73 );
  3         2  
30              
31 3     3   239 use Moo;
  3         3  
  3         15  
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 1160     1160 0 49858 my $self = shift;
88              
89 1160         2225 $self->_validate_ip_integer();
90              
91 1159         1794 my $max = $self->bits();
92 1159 100       2295 if ( $self->prefix_length() > $max ) {
93 1         81 confess $self->prefix_length()
94             . ' is not a valid IP network prefix length';
95             }
96              
97 1158         14935 return;
98             }
99              
100             sub new_from_string {
101 794     794 1 156153 my $class = shift;
102 794         1476 my %p = @_;
103              
104 794 100       1465 die 'undef is not a valid IP network' unless defined $p{string};
105              
106 790         1611 my ( $address, $prefix_length ) = split /\//, $p{string}, 2;
107              
108             my $version
109             = $p{version} ? $p{version}
110 790 100       1983 : inet_pton( AF_INET6, $address ) ? 6
    100          
111             : 4;
112              
113 790 100 100     3050 if ( $version == 6 && inet_pton( AF_INET, $address ) ) {
114 45         58 $prefix_length += 96;
115 45         62 $address = '::' . $address;
116             }
117              
118 790         1586 my $integer = _string_address_to_integer( $address, $version );
119              
120 790 100       4096 confess "$p{string} is not a valid IP network"
121             unless defined $integer;
122              
123 772         13768 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 22888 my $class = shift;
132 410         758 my %p = @_;
133              
134 410         476 my $integer = delete $p{integer};
135 410         328 my $version = delete $p{version};
136              
137 410 100 66     681 $version ||= ref $integer ? 6 : 4;
138              
139 410         6812 return $class->new(
140             _integer => $integer,
141             version => $version,
142             %p,
143             );
144             }
145              
146             sub _build_address_string {
147 728     728   5243 _integer_address_to_string( $_[0]->first_as_integer );
148             }
149              
150             sub _build_subnet_integer {
151 1112     1112   27029 my $self = shift;
152              
153 1112         1836 return $self->_prefix_length_to_mask( $self->prefix_length() );
154             }
155              
156             sub _prefix_length_to_mask {
157 13559     13559   9006 my $self = shift;
158 13559         9022 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       25337 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 1366 my $self = shift;
170              
171 250         3313 my $base = $self->first()->as_integer();
172              
173 250         4746 my $prefix_length = $self->prefix_length();
174              
175 250         450 my $bits = $self->bits;
176 250         421 while ($prefix_length) {
177 12447         11898 my $mask = $self->_prefix_length_to_mask($prefix_length);
178              
179 12447 100       28122 last if ( $base & $mask ) != $base;
180              
181 12197         17685 $prefix_length--;
182             }
183              
184 250         1327 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 1184 my $self = shift;
191              
192 5         12 my $version = $self->version();
193 5         124 my $current_ip = $self->first()->as_integer();
194 5         174 my $last_ip = $self->last()->as_integer();
195              
196             return sub {
197 284 100   284   4179 return if $current_ip > $last_ip;
198              
199 279         906 Net::Works::Address->new_from_integer(
200             integer => $current_ip++,
201             version => $version,
202             );
203 5         109 };
204             }
205              
206             sub as_string {
207 779     779 1 2000 my $self = shift;
208              
209 779         12489 return join '/', lc $self->_address_string(), $self->prefix_length();
210             }
211              
212             sub _build_first {
213 1026     1026   28261 my $self = shift;
214              
215 1026         1433 my $int = $self->first_as_integer;
216              
217 1026         29149 return Net::Works::Address->new_from_integer(
218             integer => $int,
219             version => $self->version(),
220             );
221             }
222              
223 1853     1853 1 3477 sub first_as_integer { $_[0]->_integer() & $_[0]->_subnet_integer() }
224              
225             sub _build_last {
226 769     769   154206 my $self = shift;
227              
228 769         1097 my $int = $self->last_as_integer;
229              
230 769         7110 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 2022 my $self = shift;
238              
239 1184         1951 return $self->_integer() | ( $self->_max() & ~$self->_subnet_integer() );
240             }
241              
242             sub contains {
243 46     46 1 255 my $self = shift;
244 46         36 my $thing = shift;
245              
246 46         33 my $first_integer;
247             my $last_integer;
248 46 100       158 if ( $thing->isa('Net::Works::Address') ) {
    50          
249 25         43 $first_integer = $last_integer = $thing->as_integer();
250             }
251             elsif ( $thing->isa('Net::Works::Network') ) {
252 21         28 $first_integer = $thing->first_as_integer();
253 21         349 $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     539 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 199 my $self = shift;
267              
268 34 100       67 return () if $self->prefix_length() == $self->bits();
269              
270 32         49 my $first_int = $self->first_as_integer();
271 32         822 my $last_int = $self->last_as_integer();
272              
273             return (
274 32         233 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 is_single_address {
288 12     12 1 56 my $self = shift;
289              
290 12   66     137 return ( $self->version == 4 && $self->prefix_length == 32 )
291             || ( $self->version == 6 && $self->prefix_length == 128 );
292             }
293              
294             sub range_as_subnets {
295 5     5 1 92 my $class = shift;
296 5         6 my $first_ip = shift;
297 5         6 my $last_ip = shift;
298 5 100 100 7   46 my $version = shift || ( any { /:/ } $first_ip, $last_ip ) ? 6 : 4;
  7         25  
299              
300 5 50       42 $first_ip = Net::Works::Address->new_from_string(
301             string => $first_ip,
302             version => $version,
303             ) unless ref $first_ip;
304              
305 5 50       40 $last_ip = Net::Works::Address->new_from_string(
306             string => $last_ip,
307             version => $version,
308             ) unless ref $last_ip;
309              
310 5         29 my @ranges = $class->_remove_reserved_subnets_from_range(
311             $first_ip->as_integer(),
312             $last_ip->as_integer(),
313             $version
314             );
315              
316 5         6 my @subnets;
317 5         7 for my $range (@ranges) {
318 24         30 push @subnets, $class->_split_one_range( @{$range}, $version );
  24         51  
319             }
320              
321 5         108 return @subnets;
322             }
323              
324             {
325             my @reserved_4 = qw(
326             0.0.0.0/8
327             10.0.0.0/8
328             100.64.0.0/10
329             127.0.0.0/8
330             169.254.0.0/16
331             172.16.0.0/12
332             192.0.0.0/29
333             192.0.2.0/24
334             192.88.99.0/24
335             192.168.0.0/16
336             198.18.0.0/15
337             198.51.100.0/24
338             203.0.113.0/24
339             224.0.0.0/4
340             240.0.0.0/4
341             );
342              
343             # ::/128 and ::1/128 are reserved under IPv6 but these are already covered
344             # under 0.0.0.0/8
345             my @reserved_6 = (
346             @reserved_4, qw(
347             100::/64
348             2001::/23
349             2001:db8::/32
350             fc00::/7
351             fe80::/10
352             ff00::/8
353             )
354             );
355              
356             my %reserved_networks = (
357             4 => [
358             map { [ $_->first()->as_integer(), $_->last()->as_integer() ] }
359             sort { $a->first <=> $b->first }
360             map {
361             Net::Works::Network->new_from_string(
362             string => $_,
363             version => 4
364             )
365             } @reserved_4,
366             ],
367             6 => [
368             map { [ $_->first()->as_integer(), $_->last()->as_integer() ] }
369             sort { $a->first <=> $b->first }
370             map {
371             Net::Works::Network->new_from_string(
372             string => $_,
373             version => 6
374             )
375             } @reserved_6,
376             ],
377             );
378              
379             sub _remove_reserved_subnets_from_range {
380 13     13   235 my $class = shift;
381 13         14 my $first_ip = shift;
382 13         10 my $last_ip = shift;
383 13         10 my $version = shift;
384              
385 13         11 my @ranges;
386 13         11 my $add_remaining = 1;
387              
388 13         10 for my $pn ( @{ $reserved_networks{$version} } ) {
  13         33  
389 82         77 my $reserved_first = $pn->[0];
390 82         53 my $reserved_last = $pn->[1];
391              
392 82 100       155 next if ( $reserved_last <= $first_ip );
393 74 100       95 last if ( $last_ip < $reserved_first );
394              
395 66 100       129 push @ranges, [ $first_ip, $reserved_first - 1 ]
396             if $first_ip < $reserved_first;
397              
398 66 100       80 if ( $last_ip <= $reserved_last ) {
399 5         6 $add_remaining = 0;
400 5         8 last;
401             }
402              
403 61         77 $first_ip = $reserved_last + 1;
404             }
405              
406 13 100       25 push @ranges, [ $first_ip, $last_ip ] if $add_remaining;
407              
408 13         33 return @ranges;
409             }
410             }
411              
412             sub _split_one_range {
413 24     24   28 my $class = shift;
414 24         18 my $first_ip = shift;
415 24         24 my $last_ip = shift;
416 24         21 my $version = shift;
417              
418 24         23 my @subnets;
419 24         45 while ( $first_ip <= $last_ip ) {
420 326         5603 my $max_network = _max_subnet( $first_ip, $last_ip, $version );
421              
422 326         1250 push @subnets, $max_network;
423              
424 326         423 $first_ip = $max_network->last_as_integer + 1;
425             }
426              
427 24         525 return @subnets;
428             }
429              
430             sub _max_subnet {
431 326     326   258 my $ip = shift;
432 326         247 my $maxip = shift;
433 326         219 my $version = shift;
434              
435 326 100       389 my $prefix_length = $version == 6 ? 128 : 32;
436              
437 326         216 my $v = $ip;
438 326 100       576 my $reverse_mask = $version == 6 ? uint128(1) : 1;
439              
440 326   66     1825 while (( $v & 1 ) == 0
      100        
441             && $prefix_length > 0
442             && ( $ip | $reverse_mask ) <= $maxip ) {
443              
444 19009         13886 $prefix_length--;
445 19009         17825 $v = $v >> 1;
446              
447 19009         102771 $reverse_mask = ( $reverse_mask << 1 ) | 1;
448             }
449              
450 326         596 return Net::Works::Network->new_from_integer(
451             integer => $ip,
452             prefix_length => $prefix_length,
453             version => $version,
454             );
455             }
456              
457             sub _compare_overload {
458 30     30   2246 my $self = shift;
459 30         26 my $other = shift;
460              
461             confess 'Cannot compare unless both objects are '
462             . __PACKAGE__
463             . ' objects'
464             unless blessed $self
465             && blessed $other
466 30 50 33     172 && eval { $self->isa(__PACKAGE__) && $other->isa(__PACKAGE__) };
  30 50 33     155  
467              
468 30   100     417 my $cmp = (
469             $self->first() <=> $other->first()
470             or $self->prefix_length() <=> $other->prefix_length()
471             );
472              
473 30 50       612 return shift() ? $cmp * -1 : $cmp;
474             }
475              
476             __PACKAGE__->meta()->make_immutable();
477              
478             1;
479              
480             # ABSTRACT: An object representing a single IP address (4 or 6) subnet
481              
482             __END__