File Coverage

blib/lib/Net/CIDR/Set.pm
Criterion Covered Total %
statement 210 228 92.1
branch 51 60 85.0
condition 17 29 58.6
subroutine 43 49 87.7
pod 25 26 96.1
total 346 392 88.2


line stmt bran cond sub pod time code
1             package Net::CIDR::Set;
2              
3             # ABSTRACT: Manipulate sets of IP addresses
4              
5 9     9   757304 use v5.14;
  9         29  
6 9     9   53 use warnings;
  9         33  
  9         528  
7              
8 9     9   49 use Carp qw( croak confess );
  9         10  
  9         517  
9 9     9   3511 use Net::CIDR::Set::IPv4;
  9         19  
  9         321  
10 9     9   3531 use Net::CIDR::Set::IPv6;
  9         29  
  9         271  
11              
12 9     9   4482 use overload '""' => 'as_string';
  9         12207  
  9         53  
13              
14 9     9   622 use namespace::autoclean;
  9         14  
  9         41  
15              
16             our $VERSION = '0.21';
17              
18              
19             {
20             my %type_map = (
21             ipv4 => 'Net::CIDR::Set::IPv4',
22             ipv6 => 'Net::CIDR::Set::IPv6',
23             );
24              
25             sub new {
26 113     113 1 1028280 my $self = shift;
27 113   66     289 my $class = ref $self || $self;
28 113         265 my $set = bless { ranges => [] }, $class;
29 113 50       217 my $opt = 'HASH' eq ref $_[0] ? shift : {};
30 113 50       247 if ( defined( my $type = delete $opt->{type} ) ) {
    100          
31 0   0     0 my $coder_class = $type_map{$type} || $type;
32 0         0 $set->{coder} = $coder_class->new;
33             }
34             elsif ( ref $self ) {
35 16         23 $set->{coder} = $self->{coder};
36             }
37 113         205 my @unk = keys %$opt;
38 113 50       165 croak "Unknown options: ", _and( sort @unk ) if @unk;
39 113 100       280 $set->add( @_ ) if @_;
40 110         284 return $set;
41             }
42             }
43              
44             # Return the index of the first element >= the supplied value. If the
45             # supplied value is larger than any element in the list the returned
46             # value will be equal to the size of the list.
47             sub _find_pos {
48 330     330   336 my $self = shift;
49 330         323 my $val = shift;
50 330   100     543 my $low = shift || 0;
51              
52 330         293 my $high = scalar( @{ $self->{ranges} } );
  330         377  
53              
54 330         455 while ( $low < $high ) {
55 192         246 my $mid = int( ( $low + $high ) / 2 );
56 192         218 my $cmp = $val cmp $self->{ranges}[$mid];
57 192 100       254 if ( $cmp < 0 ) {
    100          
58 43         53 $high = $mid;
59             }
60             elsif ( $cmp > 0 ) {
61 97         127 $low = $mid + 1;
62             }
63             else {
64 52         74 return $mid;
65             }
66             }
67              
68 278         296 return $low;
69             }
70              
71             sub _inc {
72 681     681   167186 my @b = reverse unpack 'C*', shift;
73 681         827 for ( @b ) {
74 2999 100       3435 last unless ++$_ == 256;
75 2319         2074 $_ = 0;
76             }
77 681         1840 return pack 'C*', reverse @b;
78             }
79              
80             sub _dec {
81 158     158   1782 my @b = reverse unpack 'C*', shift;
82 158         207 for ( @b ) {
83 605 100       794 last unless $_-- == 0;
84 448         412 $_ = 255;
85             }
86 158         534 return pack 'C*', reverse @b;
87             }
88              
89             sub _guess_coder {
90 96     96   119 my ( $self, $ip ) = @_;
91 96         130 for my $class ( qw( Net::CIDR::Set::IPv4 Net::CIDR::Set::IPv6 ) ) {
92 137         404 my $coder = $class->new;
93 137         178 ( eval { $coder->encode( $ip ) } );
  137         239  
94 137 100       3952 return $coder unless $@;
95             }
96 8         127 croak "Can't decode $ip as an IPv4 or IPv6 address";
97             }
98              
99             sub _encode {
100 159     159   202 my ( $self, $ip ) = @_;
101 159   100     592 my $cdr = $self->{coder} ||= $self->_guess_coder( $ip );
102 151         239 return $cdr->encode( $ip );
103             }
104              
105             {
106             for my $dele ( qw( _decode _nbits ) ) {
107 9     9   5287 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         13  
  9         16975  
108             my $meth = $dele =~ s/^_//r;
109             *{$dele} = sub {
110 107     107   99 my $self = shift;
111 107   33     179 my $cdr = $self->{coder} || croak "Don't know how to $meth yet";
112 107         244 return $cdr->$meth( @_ );
113             };
114             }
115             }
116              
117             sub _conjunction {
118 2     2   514 my ( $conj, @list ) = @_;
119 2         3 my $last = pop @list;
120 2         12 return join " $conj ", join( ', ', @list ), $last;
121             }
122              
123 1     1   3 sub _and { _conjunction( 'and', @_ ) }
124              
125             sub _check_and_coerce {
126 9     9   14 my ( $self, @others ) = @_;
127              
128             my %class = map {
129 20   100     20 eval { ( defined $_ && $_->nbits || '' ) => $_ }
  20         90  
130 9         14 } map { $_->{coder} } grep { defined } $self, @others;
  20         27  
  20         30  
131              
132 9         30 my @found = sort grep $_, keys %class;
133              
134 9 50       14 croak "Can't mix ", _and( @found ), " bit addresses"
135             if @found > 1;
136              
137 9   66     42 $self->{coder} ||= $class{ $found[0] };
138 9         16 return $self;
139             }
140              
141              
142             sub invert {
143 15     15 1 15 my $self = shift;
144              
145 15         20 my @pad = ( 0 ) x ( $self->_nbits / 8 );
146 15         21 my ( $min, $max ) = map { pack 'C*', $_, @pad } 0, 1;
  30         58  
147              
148 15 50       30 if ( $self->is_empty ) {
149 0         0 $self->{ranges} = [ $min, $max ];
150 0         0 return;
151             }
152              
153 15 100       25 if ( $self->{ranges}[0] eq $min ) {
154 8         9 shift @{ $self->{ranges} };
  8         10  
155             }
156             else {
157 7         9 unshift @{ $self->{ranges} }, $min;
  7         10  
158             }
159              
160 15 100       25 if ( $self->{ranges}[-1] eq $max ) {
161 8         8 pop @{ $self->{ranges} };
  8         32  
162             }
163             else {
164 7         7 push @{ $self->{ranges} }, $max;
  7         15  
165             }
166             }
167              
168              
169             sub copy {
170 16     16 1 19 my $self = shift;
171 16         36 my $copy = $self->new;
172 16         19 @{ $copy->{ranges} } = @{ $self->{ranges} };
  16         27  
  16         18  
173 16         27 return $copy;
174             }
175              
176             sub _add_range {
177 165     165   219 my ( $self, $from, $to ) = @_;
178 165         251 my $fpos = $self->_find_pos( $from );
179 165         251 my $tpos = $self->_find_pos( _inc( $to ), $fpos );
180              
181 165 100       324 $from = $self->{ranges}[ --$fpos ] if ( $fpos & 1 );
182 165 100       222 $to = $self->{ranges}[ $tpos++ ] if ( $tpos & 1 );
183              
184 165         150 splice @{ $self->{ranges} }, $fpos, $tpos - $fpos, ( $from, $to );
  165         492  
185             }
186              
187              
188             sub add {
189 99     99 1 2713 my ( $self, @addr ) = @_;
190 99         159 for my $ip ( map { split /\s*,\s*/ } @addr ) {
  126         444  
191 159 50       255 my ( $lo, $hi ) = $self->_encode( $ip )
192             or croak "Can't decode $ip";
193 149         241 $self->_add_range( $lo, $hi );
194             }
195             }
196              
197              
198             sub remove {
199 1     1 1 463 my $self = shift;
200              
201 1         5 $self->invert;
202 1         3 $self->add( @_ );
203 1         2 $self->invert;
204             }
205              
206              
207             sub merge {
208 9     9 1 9 my $self = shift;
209 9         17 $self->_check_and_coerce( @_ );
210              
211             # TODO: This isn't very efficient - and merge gets called from all
212             # sorts of other places.
213 9         14 for my $other ( @_ ) {
214 11         16 my $iter = $other->_iterate_runs;
215 11         15 while ( my ( $from, $to ) = $iter->() ) {
216 16         23 $self->_add_range( $from, $to );
217             }
218             }
219             }
220              
221              
222             *contains = *contains_all;
223              
224             sub contains_all {
225 1     1 1 599 my $self = shift;
226 1         2 my $class = ref $self;
227 1         4 return $class->new( @_ )->subset( $self );
228             }
229              
230              
231             sub contains_any {
232 0     0 1 0 my $self = shift;
233 0         0 my $class = ref $self;
234 0         0 return !$class->new( @_ )->intersection( $self )->is_empty;
235             }
236              
237             sub _iterate_runs {
238 62     62   64 my $self = shift;
239              
240 62         56 my $pos = 0;
241 62         72 my $limit = scalar( @{ $self->{ranges} } );
  62         68  
242              
243             return sub {
244 147 100   147   335 return if $pos >= $limit;
245 85         99 my @r = @{ $self->{ranges} }[ $pos, $pos + 1 ];
  85         157  
246 85         82 $pos += 2;
247 85         188 return @r;
248 62         235 };
249             }
250              
251             sub compliment {
252 0     0 0 0 croak "That's very kind of you - but I expect you meant complement";
253             }
254              
255              
256             sub complement {
257 10     10 1 17 my $new = shift->copy;
258             # TODO: What if it's empty?
259 10         19 $new->invert;
260 10         19 return $new;
261             }
262              
263              
264             sub union {
265 6     6 1 21 my $new = shift->copy;
266 6         19 $new->merge( @_ );
267 6         13 return $new;
268             }
269              
270              
271             sub intersection {
272 3     3 1 8 my $self = shift;
273 3         6 my $class = ref $self;
274 3         7 my $new = $class->new;
275 3         6 $new->merge( map { $_->complement } $self, @_ );
  6         10  
276 3         14 $new->invert;
277 3         6 return $new;
278             }
279              
280              
281             sub xor {
282 0     0 1 0 my $self = shift;
283 0         0 return $self->union( @_ )
284             ->intersection( $self->intersection( @_ )->complement );
285             }
286              
287              
288             sub diff {
289 0     0 1 0 my $self = shift;
290 0         0 my $other = shift;
291 0         0 return $self->intersection( $other->union( @_ )->complement );
292             }
293              
294              
295             sub is_empty {
296 16     16 1 17 my $self = shift;
297 16         16 return @{ $self->{ranges} } == 0;
  16         29  
298             }
299              
300              
301             sub superset {
302 0     0 1 0 my $other = pop;
303 0         0 return $other->subset( reverse( @_ ) );
304             }
305              
306              
307             sub subset {
308 0     0 1 0 my $self = shift;
309 0   0     0 my $other = shift || croak "I need two sets to compare";
310 0         0 return $self->equals( $self->intersection( $other ) );
311             }
312              
313              
314             sub equals {
315 36 50   36 1 112 return unless @_;
316              
317             # Array of array refs
318 36         50 my @edges = map { $_->{ranges} } @_;
  72         98  
319 36         40 my $medge = scalar( @edges ) - 1;
320              
321 36         42 POS: for ( my $pos = 0;; $pos++ ) {
322 114         127 my $v = $edges[0]->[$pos];
323 114 100       135 if ( defined( $v ) ) {
324 78         104 for ( @edges[ 1 .. $medge ] ) {
325 78         75 my $vv = $_->[$pos];
326 78 50 33     179 return unless defined( $vv ) && $vv eq $v;
327             }
328             }
329             else {
330 36         39 for ( @edges[ 1 .. $medge ] ) {
331 36 50       64 return if defined $_->[$pos];
332             }
333             }
334              
335 114 100       165 last POS unless defined( $v );
336             }
337              
338 36         106 return 1;
339             }
340              
341              
342             sub iterate_addresses {
343 1     1 1 3 my ( $self, @args ) = @_;
344 1         3 my $iter = $self->_iterate_runs;
345 1         2 my @r = ();
346             return sub {
347 2     2   3 while ( 1 ) {
348 3 100 100     4 @r = $iter->() or return unless @r;
349 2 100       5 return $self->_decode( ( my $last, $r[0] )
350             = ( $r[0], _inc( $r[0] ) ), @args )
351             unless $r[0] eq $r[1];
352 1         2 @r = ();
353             }
354 1         5 };
355             }
356              
357              
358             sub iterate_cidr {
359 8     8 1 17 my ( $self, @args ) = @_;
360 8         11 my $iter = $self->_iterate_runs;
361 8         25 my $size = $self->_nbits;
362 8         10 my @r = ();
363             return sub {
364 39     39   35 while ( 1 ) {
365 55 100 100     82 @r = $iter->() or return unless @r;
366 47 100       60 unless ( $r[0] eq $r[1] ) {
367 31         384 ( my $bits = unpack 'B*', $r[0] ) =~ /(0*)$/;
368 31         38 my $pad = length $1;
369 31 100       47 $pad = $size if $pad > $size;
370 31         26 while ( 1 ) {
371 205         436 my $next = _inc( $r[0] | pack 'B*',
372             ( '0' x ( length( $bits ) - $pad ) ) . ( '1' x $pad ) );
373 205 100       298 return $self->_decode( ( my $last, $r[0] ) = ( $r[0], $next ),
374             @args )
375             if $next le $r[1];
376 174         138 $pad--;
377             }
378             }
379 16         22 @r = ();
380             }
381 8         41 };
382             }
383              
384              
385             sub iterate_ranges {
386 42     42 1 68 my ( $self, @args ) = @_;
387 42         71 my $iter = $self->_iterate_runs;
388             return sub {
389 94 100   94   98 return unless my @r = $iter->();
390 52         86 return $self->_decode( @r, @args );
391 42         129 };
392             }
393              
394              
395             sub as_array {
396 51     51 1 103 my ( $self, $iter ) = @_;
397 51         56 my @addr = ();
398 51         67 while ( my $addr = $iter->() ) {
399 84         146 push @addr, $addr;
400             }
401 51         331 return @addr;
402             }
403              
404              
405             sub as_address_array {
406 1     1 1 2 my $self = shift;
407 1         5 return $self->as_array( $self->iterate_addresses( @_ ) );
408             }
409              
410              
411             sub as_cidr_array {
412 8     8 1 270 my $self = shift;
413 8         19 return $self->as_array( $self->iterate_cidr( @_ ) );
414             }
415              
416              
417             sub as_range_array {
418 42     42 1 51 my $self = shift;
419 42         69 return $self->as_array( $self->iterate_ranges( @_ ) );
420             }
421              
422              
423 36     36 1 378 sub as_string { join ', ', shift->as_range_array( @_ ) }
424              
425             1;
426              
427             __END__