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